Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/alchemy-local-dev.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@maple-dev/alchemy": minor
---

`alchemy dev` can run Maple Local for the stack. Declare `Local("observability")` from `@maple-dev/alchemy/local`, register `localProviders()`, and pass it as `Telemetry({ local })`: under `alchemy dev` every Worker exports to one shared loopback server, with no ingest key and no Maple account, and the dashboard opens at `local.maple.dev`. Deploys ignore `local` and use `ingestKey` and `endpoint` as before.

The server runs in an Alchemy sidecar process, survives stack reloads, and stores data per stack, stage and resource under `.alchemy/maple-local/`. The first run downloads the pinned chDB library, verifies its sha256 and caches it in `~/.maple/cache/libchdb`; `MAPLE_LIBCHDB` points at a copy you already have. `LocalProps` takes `dataDir`, `port` (default `4318`) and `onDirtyStore`: a store left unclean by a killed sidecar is refused unless it is `"wipe"`.

Requires Bun, and `@effect/platform-bun` as an optional peer dependency (Alchemy already needs it under Bun).
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ jobs:
cli:
- *base
- 'apps/cli/**'
- 'packages/local-server/**'
- 'apps/local-ui/**'
# local-ui renders apps/web's source modules.
- 'apps/web/src/**'
Expand All @@ -95,6 +96,7 @@ jobs:
archive:
- *base
- 'apps/cli/**'
- 'packages/local-server/**'
- 'apps/local-ui/**'
- 'scripts/build-local-binary.sh'
- 'scripts/gen-ui-embed.ts'
Expand Down
74 changes: 74 additions & 0 deletions .oxlintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,62 @@
"typescript/no-non-null-assertion": "off"
}
},
// Burndown entries relocated with their implementation from apps/cli into
// @maple/local-server, one block per rule. Drop each file as it is converted.
{
"files": [
"packages/local-server/src/archives/export.ts",
"packages/local-server/src/archives/gc.ts",
"packages/local-server/src/archives/generation.ts",
"packages/local-server/src/archives/journal.ts",
"packages/local-server/src/archives/listing.ts",
"packages/local-server/src/archives/manifest.ts",
"packages/local-server/src/archives/paths.ts",
"packages/local-server/src/chdb.ts",
"packages/local-server/src/checkpoints.ts",
"packages/local-server/src/durable-files.ts",
"packages/local-server/src/eventing/control-store.ts",
"packages/local-server/src/local-address.ts",
"packages/local-server/src/local-store-migration-journal.ts",
"packages/local-server/src/otlp/encode.ts",
"packages/local-server/src/schema-physical.ts",
"packages/local-server/src/serve.ts",
"packages/local-server/src/store-claim.ts",
"packages/local-server/src/store-version.ts"
],
"rules": { "maple/no-try-catch": "off" }
},
{
"files": [
"packages/local-server/src/archives/export.ts",
"packages/local-server/src/archives/gc.ts",
"packages/local-server/src/archives/journal.ts",
"packages/local-server/src/archives/paths.ts",
"packages/local-server/src/archives/retention.ts",
"packages/local-server/src/checkpoints.ts",
"packages/local-server/src/insert-batcher.ts",
"packages/local-server/src/local-store-migration-journal.ts",
"packages/local-server/src/otlp/encode.ts",
"packages/local-server/src/schema-manifest.ts",
"packages/local-server/src/schema-physical.ts",
"packages/local-server/src/scoped-delete.ts"
],
"rules": { "typescript/no-non-null-assertion": "off" }
},
{
"files": [
"packages/local-server/src/archives/generation.ts",
"packages/local-server/src/archives/manifest.ts",
"packages/local-server/src/archives/paths.ts",
"packages/local-server/src/archives/retention.ts",
"packages/local-server/src/checkpoints.ts",
"packages/local-server/src/eventing/control-store.ts",
"packages/local-server/src/identity-schema.ts",
"packages/local-server/src/service-map-rollup.ts",
"packages/local-server/src/store-version.ts"
],
"rules": { "maple/no-date-parse": "off" }
},
// Test cases that cannot catch a bug: assertion-free, existence-only, constant pins,
// Tailwind class pins, source greps, exact command lists and SQL projection pins.
// Rationale per rule in scripts/oxlint-plugins/test-slop.mjs.
Expand Down Expand Up @@ -707,6 +763,24 @@
}
]
}
},
// @maple/local-server is bundled into @maple-dev/alchemy's sidecar; the CLI, its
// migrations and its historical schema snapshots must never be reachable from it.
{
"files": ["packages/local-server/src/**/*.ts"],
"rules": {
"no-restricted-imports": [
"error",
{
"patterns": [
{
"regex": "^@maple/cli($|/)|/apps/cli/",
"message": "@maple/local-server must not depend on the CLI; the CLI depends on it."
}
]
}
]
}
}
],
// Vendored third-party source is read as a unit and re-pulled wholesale from its upstream,
Expand Down
8 changes: 4 additions & 4 deletions apps/cli/bench/ingest-chdb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@
import { mkdtempSync, rmSync } from "node:fs"
import { tmpdir } from "node:os"
import { join } from "node:path"
import { Chdb, writeIngestConfig } from "../src/server/chdb"
import { buildInsertStatements } from "../src/server/inserts"
import { type EncodedBatch, encodeLogs, encodeMetrics, encodeTraces } from "../src/server/otlp/encode"
import { LOCAL_SCHEMA_SQL } from "../src/server/schema-identity"
import { Chdb, writeIngestConfig } from "@maple/local-server/chdb"
import { buildInsertStatements } from "@maple/local-server/inserts"
import { type EncodedBatch, encodeLogs, encodeMetrics, encodeTraces } from "@maple/local-server/otlp/encode"
import { LOCAL_SCHEMA_SQL } from "@maple/local-server/schema-identity"
import { type Signal, exportBody } from "./payloads"
import { environment, flag, list, numbers, summarize, type Summary, writeReport } from "./report"

Expand Down
7 changes: 3 additions & 4 deletions apps/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,17 @@
"start": "bun run src/bin.ts",
"bench:ingest": "bun bench/ingest-http.ts",
"bench:ingest:chdb": "bun bench/ingest-chdb.ts",
"test": "bun test --max-concurrency=1 test/*.test.ts src/server/otlp/encode.test.ts src/core/*.test.ts",
"test": "bun test --max-concurrency=1 test/*.test.ts src/core/*.test.ts",
"typecheck": "tsrs --noEmit"
},
"dependencies": {
"@effect/platform-bun": "catalog:effect",
"@maple-dev/effect-sdk": "workspace:*",
"@maple/domain": "workspace:*",
"@maple/eventing-core": "workspace:*",
"@maple/ingest-rules": "workspace:*",
"@maple/local-server": "workspace:*",
"@maple/query-engine": "workspace:*",
"effect": "catalog:effect",
"protobufjs": "^8.8.1"
"effect": "catalog:effect"
},
"devDependencies": {
"@effect/vitest": "catalog:effect",
Expand Down
8 changes: 4 additions & 4 deletions apps/cli/src/bin.ts
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,11 @@ import { maybeNotifyUpdate } from "./core/update"
import { maybeNotifyWhatsNew } from "./core/changelog"
import { LocalAccessHttpClient } from "./core/local-access"
import { WarehouseExecutorFromMode } from "./core/warehouse"
import { debugEnabled } from "./lib/debug"
import { debugEnabled } from "@maple/local-server/debug-log"
import { makeHelpCapture, usageHint } from "./lib/help"
import { archiveErrorMessage } from "./server/archives/errors"
import { CHECKPOINT_REOPEN_PROBE_ENV, validateCheckpointDataDir } from "./server/checkpoints"
import { MAPLE_VERSION } from "./version"
import { archiveErrorMessage } from "@maple/local-server/archives/errors"
import { CHECKPOINT_REOPEN_PROBE_ENV, validateCheckpointDataDir } from "@maple/local-server/checkpoints"
import { MAPLE_VERSION } from "@maple/local-server/version"

// WarehouseExecutorFromMode needs Mode (which needs MapleConfig). provideMerge
// keeps Mode + MapleConfig in the output context too, so the login/logout/whoami
Expand Down
22 changes: 13 additions & 9 deletions apps/cli/src/commands/archive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,25 @@ import * as Argument from "effect/cli/Argument"
import { randomUUID } from "node:crypto"
import { homedir } from "node:os"
import { join, resolve } from "node:path"
import { createArchiveGeneration, runArchiveReconciliation } from "../server/archives/generation"
import { createArchiveGeneration, runArchiveReconciliation } from "@maple/local-server/archives/generation"
import {
listActiveGenerations,
activeParquetPaths,
rebuildCatalogWithMaintenanceLock,
verifyActiveGenerations,
} from "../server/archives/listing"
import { runArchiveGc } from "../server/archives/gc"
import { resolveArchiveTuning, tuningRecord } from "../server/archives/config"
import { ARCHIVE_SIGNALS, isArchiveSignalName, type ArchiveSignalName } from "../server/archives/signals"
import { expireArchiveDay, readRetiredDayLedger, retireLiveDay } from "../server/archives/retention"
import { validateRangeDate } from "../server/archives/paths"
import { maintenanceOperation } from "../server/checkpoints"
} from "@maple/local-server/archives/listing"
import { runArchiveGc } from "@maple/local-server/archives/gc"
import { resolveArchiveTuning, tuningRecord } from "@maple/local-server/archives/config"
import {
ARCHIVE_SIGNALS,
isArchiveSignalName,
type ArchiveSignalName,
} from "@maple/local-server/archives/signals"
import { expireArchiveDay, readRetiredDayLedger, retireLiveDay } from "@maple/local-server/archives/retention"
import { validateRangeDate } from "@maple/local-server/archives/paths"
import { maintenanceOperation } from "@maple/local-server/checkpoints"
import { amber, bold, dim, green, red } from "../lib/style"
import { ArchiveError } from "../server/archives/errors"
import { ArchiveError } from "@maple/local-server/archives/errors"
import { debugLog } from "../lib/debug"
import { jsonFormatRequested, writeJson } from "./json-output"

Expand Down
2 changes: 1 addition & 1 deletion apps/cli/src/commands/changelog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { stripV } from "../core/update"
import { CliUsageError } from "../lib/errors"
import { chosenFormat, printJson } from "../lib/output"
import { bold } from "../lib/style"
import { MAPLE_VERSION } from "../version"
import { MAPLE_VERSION } from "@maple/local-server/version"

const VERSION_RE = /^v?\d+\.\d+\.\d+$/

Expand Down
11 changes: 6 additions & 5 deletions apps/cli/src/commands/delete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@ import { Mode } from "../core/mode"
import { parseTimestampMs, sinceToMs } from "../core/time"
import { CliUsageError } from "../lib/errors"
import { bold, dim, green } from "../lib/style"
import { isLoopbackHostname } from "../lib/local-address"
import { maintenanceTokenPath } from "../server/archives/retention"
import { hasSubject, ScopedDeleteReport, ScopedDeleteRequest } from "../server/scoped-delete"
import { isLoopbackHostname } from "@maple/local-server/local-address"
import { maintenanceTokenPath } from "@maple/local-server/archives/retention"
import { hasSubject, ScopedDeleteReport, ScopedDeleteRequest } from "@maple/local-server/scoped-delete"
import { jsonFormatRequested, writeJson } from "./json-output"
import { serverDiscoveryPath } from "../server/store-version"
import { isProcessAlive, LocalStatus, prettyPath, ServerDiscovery } from "./server-args"
import { serverDiscoveryPath } from "@maple/local-server/store-version"
import { isProcessAlive } from "@maple/local-server/store-claim"
import { LocalStatus, prettyPath, ServerDiscovery } from "./server-args"

const LOCAL_ONLY = "maple delete removes data from a local store only"

Expand Down
6 changes: 3 additions & 3 deletions apps/cli/src/commands/in-memory-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import { Clock, Effect, Option } from "effect"
import { mkdtempSync, readdirSync, readFileSync, rmSync, statfsSync, statSync } from "node:fs"
import { tmpdir } from "node:os"
import { join } from "node:path"
import { SERVER_PID_NAME } from "../server/store-version"
import { ServerStateFileError } from "./server-errors"
import { isProcessAlive } from "./server-args"
import { SERVER_PID_NAME } from "@maple/local-server/store-version"
import { isProcessAlive } from "@maple/local-server/store-claim"
import { ServerStateFileError } from "@maple/local-server/store-errors"

/**
* `maple start --in-memory`: a throwaway store for one run.
Expand Down
4 changes: 2 additions & 2 deletions apps/cli/src/commands/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@ import {
runLocalStoreMigration,
type MigrationPlan,
} from "../server/local-store-migrations"
import { maintenanceOperation, releasePreservedCheckpoints } from "../server/checkpoints"
import { maintenanceOperation, releasePreservedCheckpoints } from "@maple/local-server/checkpoints"
import {
listRetainedLeftovers,
pruneRetainedLeftovers,
type RetainedInventory,
type RetainedLeftover,
} from "../server/retained-leftovers"
import { isStoreDirty, readMarker } from "../server/store-version"
import { isStoreDirty, readMarker } from "@maple/local-server/store-version"
import { jsonFormatRequested, writeJson } from "./json-output"
import { commandScope, mapleCommand } from "./server-args"

Expand Down
42 changes: 2 additions & 40 deletions apps/cli/src/commands/server-args.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Schema } from "effect"
import { homedir } from "node:os"
import { join, resolve, sep } from "node:path"
import { DEFAULT_LOCAL_PORT, normalizeHost, resolveBindHost } from "../lib/local-address"
import { DEFAULT_LOCAL_PORT, normalizeHost, resolveBindHost } from "@maple/local-server/local-address"

export type DirtyStorePolicy = "wipe" | "fail" | "restore-checkpoint"

Expand All @@ -16,7 +16,7 @@ export {
serverProbeUrl,
serverUrl,
validateHost,
} from "../lib/local-address"
} from "@maple/local-server/local-address"

export const defaultDataDir = (): string => join(homedir(), ".maple", "data")

Expand Down Expand Up @@ -113,23 +113,6 @@ export const ServerDiscovery = Schema.Struct({
})
export type ServerDiscovery = typeof ServerDiscovery.Type

/** Parse `ps -o etime=` (`[[dd-]hh:]mm:ss`) into whole seconds. */
export const parseElapsedSeconds = (raw: string): number | undefined => {
const match = raw.trim().match(/^(?:(\d+)-)?(?:(\d+):)?(\d+):(\d+)$/)
if (!match) return undefined
const [, days, hours, minutes, seconds] = match
return Number(days ?? 0) * 86_400 + Number(hours ?? 0) * 3_600 + Number(minutes) * 60 + Number(seconds)
}

/**
* Could a live process that started at `startedAtMs` have written a PID file
* last modified at `writtenAtMs`? A reused PID belongs to a process started
* after the writer died, so after the write. The slack covers `etime`'s
* whole-second resolution.
*/
export const startedBeforeWrite = (startedAtMs: number, writtenAtMs: number): boolean =>
startedAtMs <= writtenAtMs + 2_000

/** Does telemetry ingested since `sinceMs` need a fresh checkpoint? Unknown
* ingest state (`undefined`) always does. */
export const ingestedSince = (lastIngestAtMs: number | null | undefined, sinceMs: number): boolean =>
Expand Down Expand Up @@ -215,24 +198,3 @@ export const buildCheckpointChildArgs = (options: {
"--data-dir",
options.dataDir,
]

/** Liveness probe via signal 0: a process primitive with no FileSystem
* equivalent. Never throws (errors mean "not alive").
*
* A PID file naming *this* process is stale by definition: the previous server
* is the one that wrote it, so if its number is ours it has died and the OS
* reused the PID. In a container that is the common case, not the edge case:
* `maple start` is PID 1 on every restart, the file survives on the data
* volume, and `kill(1, 0)` always succeeds, so without this check a container
* whose server died once refuses to start forever ("already running (PID 1)").
* Non-positive PIDs are rejected for the same reason: `kill(0, 0)` and
* `kill(-n, 0)` signal process groups and "succeed" for a group that exists. */
export const isProcessAlive = (pid: number): boolean => {
if (!Number.isInteger(pid) || pid <= 0 || pid === process.pid) return false
try {
process.kill(pid, 0)
return true
} catch {
return false
}
}
Loading