From 55815c520fdc40457a6b534c032598e3892da3da Mon Sep 17 00:00:00 2001 From: djgrant <1670902+djgrant@users.noreply.github.com> Date: Wed, 22 Jul 2026 07:46:01 +0100 Subject: [PATCH] Add durable YieldStar reconciliation --- .changeset/reconciler.md | 4 +- docs/internals/reconciler.md | 19 + docs/internals/state.md | 10 + docs/manual/introduction.md | 3 +- docs/manual/reconciler.md | 69 +-- docs/rfcs/reconciler.md | 104 +--- examples/reconciler/README.md | 14 +- examples/reconciler/package.json | 5 +- examples/reconciler/src/index.ts | 59 +- packages/reconciler/package.json | 7 +- packages/reconciler/src/index.ts | 3 +- packages/reconciler/src/yieldstar.ts | 557 ++++++++++++++++++ .../test/yieldstar.integration.test.ts | 272 +++++++++ pnpm-lock.yaml | 117 ++-- pnpm-workspace.yaml | 4 + 15 files changed, 1023 insertions(+), 224 deletions(-) create mode 100644 packages/reconciler/src/yieldstar.ts create mode 100644 packages/reconciler/test/yieldstar.integration.test.ts diff --git a/.changeset/reconciler.md b/.changeset/reconciler.md index 409f877..3ddd372 100644 --- a/.changeset/reconciler.md +++ b/.changeset/reconciler.md @@ -9,6 +9,4 @@ "@notation/state-sqlite": minor --- -Add the reconciler API, versioned event streams, renewable mutation leases, -SQLite state, backend-neutral dashboard state, and compiled infrastructure -graphs. +Add the reconciler API, versioned event streams, renewable mutation leases, SQLite state, backend-neutral dashboard state, compiled infrastructure graphs, and durable YieldStar 0.5.0 reconciliation for Node.js runtimes. diff --git a/docs/internals/reconciler.md b/docs/internals/reconciler.md index 906273d..dc0e93c 100644 --- a/docs/internals/reconciler.md +++ b/docs/internals/reconciler.md @@ -4,6 +4,25 @@ The reconciler runs deployment operations to transition infrastructure from its Source: `@notation/reconciler` +## Durable workflow boundary + +`reconcileWithYieldStar` is the durable Node.js integration. It is an async generator intended to be composed inside an application-owned YieldStar workflow: + +```ts +const deploy = workflow(async function* (step, event) { + yield* reconcileWithYieldStar(step, { + deploymentId: "production", + executionId: event.executionId, + resources, + state, + }); +}); +``` + +The host owns runtime wiring and scheduling. Notation owns graph ordering, decisions, provider calls, drift reads, state persistence, and orphan lifecycle. Provider calls and state mutations are YieldStar steps, so completed calls are replayed instead of repeated after a process crash. + +The synchronous `Reconciler` described below remains the CLI path for this release. + ## Deploy flow ```ts [packages/reconciler/src/index.ts] diff --git a/docs/internals/state.md b/docs/internals/state.md index 44d5df0..9109ac0 100644 --- a/docs/internals/state.md +++ b/docs/internals/state.md @@ -78,6 +78,16 @@ Stores state and leases in SQLite. Select it in the CLI by setting const state = new SqliteStateBackend(".notation/state.db"); ``` +### `YieldStarStateBackend` + +The durable workflow integration stores resources in YieldStar 0.5.0 stores and runs on the Node SQLite runtime. Each resource is a live store; a missing store means a missing resource. + +```ts +const state = new YieldStarStateBackend(storeClient, "production"); +``` + +YieldStar assigns a UUIDv7 `instanceId` when the store is created and increments its version on update. Conditional workflow updates and deletes compare both values, preventing a stale snapshot from modifying a deleted and recreated resource. The one-based value exposed as `StateNode.rev` is derived from the authoritative YieldStar store version. + ### `StateBackend` interface All backends implement the same interface: diff --git a/docs/manual/introduction.md b/docs/manual/introduction.md index bb629e0..4a833dc 100644 --- a/docs/manual/introduction.md +++ b/docs/manual/introduction.md @@ -13,8 +13,7 @@ todoRouter.get("/todos", getTodos); Notation is a compiler, reconciler, and deployment engine. -The reconciler is also available as an embedded library. A Node.js host can construct -resources, choose a state backend, and run plan, deploy, or destroy without the CLI. +The reconciler is also available as an embedded library. A Node.js host can construct resources and compose durable reconciliation inside its own YieldStar workflow without the CLI. The compiler runs two passes over your codebase: diff --git a/docs/manual/reconciler.md b/docs/manual/reconciler.md index 87b02f7..2dd96fb 100644 --- a/docs/manual/reconciler.md +++ b/docs/manual/reconciler.md @@ -1,55 +1,34 @@ # Reconciler -Use the reconciler directly when a Node.js application needs to deploy resources without -starting the Notation CLI. - -This complete program deploys two static sites and keeps their deployment state in -SQLite: +Use `reconcileWithYieldStar` when a Node.js application needs durable resource reconciliation without starting the Notation CLI. Notation supplies reconciliation decisions and resource lifecycle operations; the application owns the outer YieldStar workflow and chooses the runtime. ```ts -import { Reconciler, createResourceRegistry } from "@notation/reconciler"; -import { SqliteStateBackend } from "@notation/state-sqlite"; -import { StaticSite } from "./static-site"; - -const state = new SqliteStateBackend("sites.db"); - -const resources = [ - new StaticSite({ - id: "documentation", - config: { - siteDirectory: "sites/docs", - html: "

Documentation

\n", - }, - }), - new StaticSite({ - id: "status", - config: { - siteDirectory: "sites/status", - html: "

All systems operational

\n", - }, - }), -]; - -const reconciler = new Reconciler({ - state, - registry: createResourceRegistry([StaticSite]), +import { SqliteSchedulerClient, SqliteStoreClient, SqliteTaskQueueClient, SqliteTimersClient, createSqliteDb } from "@yieldstar/sqlite-runtime/node"; +import { YieldStarStateBackend, reconcileWithYieldStar } from "@notation/reconciler"; +import { workflow } from "yieldstar"; + +const database = createSqliteDb({ path: ".notation/workflows.db" }); +const schedulerClient = new SqliteSchedulerClient({ + taskQueueClient: new SqliteTaskQueueClient(database), + timersClient: new SqliteTimersClient(database), +}); +const storeClient = new SqliteStoreClient({ db: database, schedulerClient }); +const state = new YieldStarStateBackend(storeClient, "my-application"); + +export const deploy = workflow(async function* (step, event) { + yield* reconcileWithYieldStar(step, { + deploymentId: "my-application", + executionId: event.executionId, + resources, + state, + }); }); - -try { - await reconciler.deploy(resources); -} finally { - state.close(); -} ``` -`StaticSite` contains the provider operations which create, read, update, and delete a -site. A real provider would call its infrastructure API instead of writing local files. +The outer workflow supplies durable step execution, timers, shared stores, waiting, and scheduling. `reconcileWithYieldStar` uses those primitives to cache completed provider calls, retry provider waiting without holding a process, persist state conditionally, delete state conditionally, and serialize deployments with `store.take`. -Pass the complete desired set to `deploy`. A resource which remains in deployment state -but is absent from that set is deleted. The explicit registry lets the reconciler find -its delete operation. +Each live resource is one YieldStar store. Absence is represented by no store, not a tombstone. YieldStar's UUIDv7 store `instanceId` and version are authoritative for conditional update and delete; Notation exposes the version as the resource state's `rev` for its existing state contract. -Notation's state records what was deployed. It does not replace application data which -owns the desired configuration. +Pass the complete desired set on every invocation. Persisted resources absent from that set are deleted through the supplied resource registry. -The runnable version is in `examples/reconciler`. +The runnable Node SQLite version is in `examples/reconciler`. diff --git a/docs/rfcs/reconciler.md b/docs/rfcs/reconciler.md index dd33e92..87056a7 100644 --- a/docs/rfcs/reconciler.md +++ b/docs/rfcs/reconciler.md @@ -1,102 +1,30 @@ -# RFC: Reconciler +# RFC: Durable YieldStar reconciliation -**Status:** implemented -**Scope:** `@notation/state`, `@notation/reconciler` +**Status:** implemented release slice +**Scope:** `@notation/reconciler`, YieldStar 0.5.0 -Notation evaluates an infrastructure program into resources, then reconciles those -resources against recorded state. The same engine now runs behind the CLI, the dashboard, -and direct library integrations. +Notation describes reconciliation and resource lifecycle operations. A host-owned YieldStar workflow supplies durable execution, waiting, state, and coordination by calling `yield* reconcileWithYieldStar(step, options)`. -```ts -import { Reconciler } from "@notation/reconciler"; -import { SqliteStateBackend } from "@notation/state-sqlite"; +## Boundary -const state = new SqliteStateBackend(".notation/state.db"); -const reconciler = new Reconciler({ state }); +Live resource objects remain in the workflow worker. They are not serialized into workflow parameters. This keeps provider clients and operation closures under application control while YieldStar persists step results and shared state. -await reconciler.deploy(resources); -state.close(); -``` +Provider create, update, read, and delete calls are durable steps with stable resource-scoped keys. A process crash after a completed create replays the cached result and continues at state persistence instead of creating the provider resource again. Retryable provider conditions become YieldStar delays, allowing the process to stop until the scheduler wakes the execution. -The reconciler boundary consists of live resource objects, a state backend, and an event -subscriber. Resource operations run in the host process. +## State lifecycle -## State +`YieldStarStateBackend` stores one live resource per `notation/resource-state` store. The store ID is scoped by deployment and resource ID. Store absence is resource absence; no application tombstone is created. -Each state record carries a revision. Updates and deletes can require the revision which -the caller previously read: +The runtime-assigned UUIDv7 `instanceId` distinguishes a deleted store from a later store created under the same logical ID. YieldStar's version is the concurrency token and is exposed as Notation's one-based `rev`. Workflow updates use `store.updateFrom` and deletes use `store.deleteFrom`, so both the instance and version must match the snapshot that informed the operation. -```ts -await state.update(resource.id, patch, resource.rev); -``` +`values` uses YieldStar 0.5.0's merged `listStores` lifecycle API, and administrative cleanup uses `deleteStore`. -A stale writer receives `RevConflict`. A missing record has revision zero, so -`expectedRev: 0` means that the record must not exist. +## Coordination -The reconciler also takes a renewable per-resource lease before it reads a resource for -mutation. The lease remains held across the provider operation and state write. Two -hosts therefore cannot create or update the same resource concurrently through the same -backend. +Each deployment has a `notation/deployment-coordination` store. The workflow atomically claims it with `store.take`. A concurrent execution suspends as a durable waiter and is woken when the holder releases the store. The same execution can recover an acquisition across the store-commit/heap-write crash gap through YieldStar's applied-step ledger. -Orphan deletion takes an additional snapshot lease. The snapshot remains stable while -the reconciler decides which state records no longer appear in the desired graph. +## Release boundary -## Backends +This slice delivers durable deploy reconciliation, drift handling, orphan deletion, Node SQLite execution, external state access, conditional persistence, and concurrent deployment serialization. The existing synchronous `Reconciler` remains the CLI execution path in this release. -`@notation/state` provides file and memory backends. `@notation/state-sqlite` provides -the reference database backend. - -Every backend implements the same contract: - -```ts -interface StateBackend { - get(id: string): Promise; - has(id: string): Promise; - update( - id: string, - patch: Partial, - expectedRev?: number, - ): Promise<{ rev: number }>; - delete(id: string, expectedRev?: number): Promise; - values(): Promise; - lease(scope: string, ttl: number): Promise; -} -``` - -The dashboard reads this interface. It does not inspect a state file directly. - -## Events - -The reconciler accepts one subscriber: - -```ts -const reconciler = new Reconciler({ - state, - emit: async (event) => auditLog.write(event), -}); -``` - -`createNdjsonEventEmitter` adapts the subscriber to a versioned newline-delimited JSON -stream. The CLI uses the same adapter for `deploy --json` and `destroy --json`. - -## Package boundary - -The CLI creates resources from compiled Notation programs, then hands those live objects -to `Reconciler`. An application can construct the same resource classes directly. - -The reconciler does not serialise resource classes or execute operations in another -process. Detached execution needs manifests, resource-reference encoding, actuator -binding, and a runtime consumer. That work has its own RFC and release. - -## Acceptance - -The reconciler example is the compatibility test for this boundary. It must: - -1. Construct a resource without the CLI. -2. Plan and deploy it through `Reconciler`. -3. Close and reopen SQLite state. -4. Plan and apply an update. -5. Receive versioned events. -6. Destroy the resource and remove its state. - -The example lives in `examples/reconciler` and runs without cloud credentials. +The next stacked phase will move CLI deploy and destroy onto a resident workflow runtime, add durable destroy as a first-class workflow operation, and fan independent dependency-level resources into coordinated child executions. diff --git a/examples/reconciler/README.md b/examples/reconciler/README.md index 428eea3..2af91ca 100644 --- a/examples/reconciler/README.md +++ b/examples/reconciler/README.md @@ -1,12 +1,8 @@ -# Reconciler +# Durable reconciler -This example deploys two static sites from an ordinary Node.js program. It does not -compile a Notation project or start the Notation CLI. +This example deploys two static sites from an ordinary Node.js program using YieldStar 0.5.0 for durable execution, state, retries, waiting, and deployment coordination. -[`src/index.ts`](./src/index.ts) is the complete program. It defines the desired -resources inline, opens a SQLite state backend, and passes the resources directly to the -reconciler. [`src/static-site.ts`](./src/static-site.ts) defines the local provider -operations used to create, read, update, and delete each site. +[`src/index.ts`](./src/index.ts) owns the outer workflow and Node SQLite runtime. It passes YieldStar's `step` context to `reconcileWithYieldStar`, while [`src/static-site.ts`](./src/static-site.ts) contains only the desired resources and provider lifecycle operations. Run it from the repository root: @@ -14,9 +10,7 @@ Run it from the repository root: pnpm --filter reconciler-example demo ``` -The generated sites are written to `sites/`, and deployment state is stored in -`sites.db`. Change the resource configuration and run the command again to update the -sites. Remove a resource from the array and run it again to delete that site. +The generated sites are written to `sites/`, and the workflow heap, resource stores, timers, and coordination state are stored in `sites.db`. Change the resource configuration and run the command again to update the sites. Remove a resource from the array and run it again to delete that site. Run the integration test with: diff --git a/examples/reconciler/package.json b/examples/reconciler/package.json index 754b89a..c5d8f2e 100644 --- a/examples/reconciler/package.json +++ b/examples/reconciler/package.json @@ -11,7 +11,10 @@ "dependencies": { "@notation/reconciler": "workspace:*", "@notation/resource": "workspace:*", - "@notation/state-sqlite": "workspace:*" + "@yieldstar/core": "0.5.0", + "@yieldstar/sqlite-runtime": "0.5.0", + "pino": "^9.9.0", + "yieldstar": "0.5.0" }, "devDependencies": { "@types/node": "^22.13.4", diff --git a/examples/reconciler/src/index.ts b/examples/reconciler/src/index.ts index 4a85756..b17146e 100644 --- a/examples/reconciler/src/index.ts +++ b/examples/reconciler/src/index.ts @@ -1,8 +1,30 @@ -import { Reconciler, createResourceRegistry } from "@notation/reconciler"; -import { SqliteStateBackend } from "@notation/state-sqlite"; +import { WorkflowRunner } from "@yieldstar/core"; +import { + SqliteHeapClient, + SqliteSchedulerClient, + SqliteStoreClient, + SqliteTaskQueueClient, + SqliteTimersClient, + createSqliteDb, +} from "@yieldstar/sqlite-runtime/node"; +import { + YieldStarStateBackend, + createResourceRegistry, + reconcileWithYieldStar, +} from "@notation/reconciler"; +import pino from "pino"; +import { createWorkflowRouter, workflow } from "yieldstar"; import { StaticSite } from "./static-site"; -const state = new SqliteStateBackend("sites.db"); +const logger = pino(); +const database = createSqliteDb({ path: "sites.db" }); +const taskQueueClient = new SqliteTaskQueueClient(database); +const schedulerClient = new SqliteSchedulerClient({ + taskQueueClient, + timersClient: new SqliteTimersClient(database), +}); +const storeClient = new SqliteStoreClient({ db: database, schedulerClient }); +const state = new YieldStarStateBackend(storeClient, "static-sites"); const resources = [ new StaticSite({ @@ -21,13 +43,34 @@ const resources = [ }), ]; -const reconciler = new Reconciler({ - state, - registry: createResourceRegistry([StaticSite]), +const deploy = workflow(async function* (step, event) { + yield* reconcileWithYieldStar(step, { + deploymentId: "static-sites", + executionId: event.executionId, + resources, + state, + registry: createResourceRegistry([StaticSite]), + }); +}); + +const runner = new WorkflowRunner({ + router: createWorkflowRouter({ deploy }), + heapClient: new SqliteHeapClient(database), + storeClient, + schedulerClient, + logger, }); try { - await reconciler.deploy(resources); + await runner.run( + { + workflowId: "deploy", + executionId: crypto.randomUUID(), + params: {}, + context: new Map(), + }, + logger, + ); } finally { - state.close(); + database.close(); } diff --git a/packages/reconciler/package.json b/packages/reconciler/package.json index 39ed2f0..3429d0d 100644 --- a/packages/reconciler/package.json +++ b/packages/reconciler/package.json @@ -14,7 +14,12 @@ "dependencies": { "@notation/resource": "workspace:*", "@notation/state": "workspace:*", + "@yieldstar/core": "0.5.0", "deep-object-diff": "^1.1.9", - "yieldstar": "^0.4.6" + "yieldstar": "0.5.0" + }, + "devDependencies": { + "@yieldstar/sqlite-runtime": "0.5.0", + "pino": "^9.9.0" } } diff --git a/packages/reconciler/src/index.ts b/packages/reconciler/src/index.ts index 44fc402..8274b60 100644 --- a/packages/reconciler/src/index.ts +++ b/packages/reconciler/src/index.ts @@ -1,7 +1,7 @@ export type ResourceApi = typeof import("@notation/resource"); export type StateApi = typeof import("@notation/state"); export type DeepObjectDiffApi = typeof import("deep-object-diff"); -export type YieldstarApi = typeof import("yieldstar"); +export type YieldStarApi = typeof import("yieldstar"); export * from "./resource-registry"; export * from "./operations"; @@ -10,3 +10,4 @@ export * from "./plan"; export * from "./reconciler"; export * from "./logger-subscriber"; export * from "./protocol"; +export * from "./yieldstar"; diff --git a/packages/reconciler/src/yieldstar.ts b/packages/reconciler/src/yieldstar.ts new file mode 100644 index 0000000..24375ba --- /dev/null +++ b/packages/reconciler/src/yieldstar.ts @@ -0,0 +1,557 @@ +import type { BaseResource, ResourceType } from "@notation/resource"; +import { RevConflict, type StateNode } from "@notation/state"; +import { type StandardSchemaV1, type StoreClient } from "@yieldstar/core"; +import { + RetryableError, + defineStore, + type WorkflowFn, + type WorkflowStore, +} from "yieldstar"; +import { buildResourceDepthLevels } from "./dependency-graph"; +import { decideAction, type ResourceAction } from "./plan"; +import { + DEFAULT_READ_POLL_OPTIONS, + DEFAULT_RETRY_OPTIONS, + matchError, + type PollOptions, +} from "./operations"; +import { + createMissingResourceRegistryMatchWarningEvent, + createResourceRegistryFromResources, + resolveResourceClass, + type ResourceRegistry, +} from "./resource-registry"; +import type { ReconcilerEventEmitter } from "./reconciler"; + +type StoredResourceState = Omit; +type CoordinationState = { holder: string | null }; + +const storedResourceStateSchema = plainObjectSchema( + "Stored resource state", + (value) => + typeof value.id === "string" && + typeof value.type === "string" && + isPlainObject(value.config) && + isPlainObject(value.params) && + isPlainObject(value.output), +); +const coordinationStateSchema = plainObjectSchema( + "Deployment coordination state", + (value) => + "holder" in value && + (value.holder === null || typeof value.holder === "string"), +); + +export const yieldStarResourceStateStore = defineStore( + "notation/resource-state", + storedResourceStateSchema, +); + +export const yieldStarDeploymentCoordinationStore = defineStore( + "notation/deployment-coordination", + coordinationStateSchema, +); + +type YieldStarStep = Parameters>[0]; + +export type YieldStarReconciliationOptions = { + deploymentId: string; + executionId: string; + resources: BaseResource[]; + state: YieldStarStateBackend; + registry?: ResourceRegistry; + dryRun?: boolean; + driftDetection?: boolean; + emit?: ReconcilerEventEmitter; + retryOptions?: PollOptions; + readPollOptions?: PollOptions; +}; + +/** + * Reconciles resources as a custom YieldStar step. The caller owns the outer + * workflow and runtime; Notation owns resource decisions and lifecycle calls. + */ +export async function* reconcileWithYieldStar( + step: YieldStarStep, + opts: YieldStarReconciliationOptions, +): AsyncGenerator { + const coordination = yield* step.store(yieldStarDeploymentCoordinationStore, { + id: opts.deploymentId, + initial: { holder: null }, + }); + + yield* coordination.take( + "notation:coordination:acquire", + (state) => state.holder === null || state.holder === opts.executionId, + (draft) => { + draft.holder = opts.executionId; + }, + ); + + try { + const resourceById = new Map( + opts.resources.map((resource) => [resource.id, resource]), + ); + + for (const level of buildResourceDepthLevels(opts.resources)) { + for (const resource of level) { + yield* reconcileResource(step, resource, opts); + } + } + + const persisted = yield* step.run("notation:orphans:list", () => + opts.state.values(), + ); + const registry = + opts.registry ?? createResourceRegistryFromResources(opts.resources); + + for (const node of persisted) { + if (resourceById.has(node.id)) continue; + + const Resource = resolveResourceClass( + registry, + node.type as ResourceType, + ); + if (!Resource) { + yield* emitDurably( + step, + `notation:orphan:${node.id}:warning`, + opts.emit, + () => + createMissingResourceRegistryMatchWarningEvent({ + workflow: "deploy", + resourceId: node.id, + resourceType: node.type as ResourceType, + }), + ); + continue; + } + + const resource = new Resource({ id: node.id, config: node.config }); + resource.setOutput(node.output); + yield* deleteResource(step, resource, opts, "orphan"); + } + } finally { + yield* coordination.update("notation:coordination:release", (draft) => { + if (draft.holder === opts.executionId) draft.holder = null; + }); + } +} + +async function* reconcileResource( + step: YieldStarStep, + resource: BaseResource, + opts: YieldStarReconciliationOptions, +): AsyncGenerator { + const prefix = `notation:resource:${resource.id}`; + let stateNode = yield* step.run(`${prefix}:state:lookup`, () => + opts.state.get(resource.id), + ); + let stateStore: WorkflowStore | undefined; + let snapshot: + Awaited> | undefined; + if (stateNode) { + stateStore = yield* openResourceState(step, opts.state, resource.id); + snapshot = yield* stateStore.get(`${prefix}:state:get`); + stateNode = toStateNode(snapshot); + } + if (stateNode) resource.setOutput(stateNode.output); + + const params = yield* step.run(`${prefix}:params`, () => + resource.getParams(), + ); + let action: ResourceAction = decideAction({ + resource, + stateNode: stateNode ?? undefined, + params, + }); + + if (action.decision === "noop" && (opts.driftDetection ?? true)) { + const remote = yield* readRemote( + step, + resource, + opts, + `${prefix}:drift-read`, + ); + action = decideAction({ + resource, + stateNode: stateNode ?? undefined, + params, + driftRead: + remote.status === "not-found" + ? remote + : { status: "found", output: remote.output }, + }); + } + + yield* emitDurably(step, `${prefix}:decision`, opts.emit, () => ({ + level: "info", + event: "reconciler.deploy.decision", + resourceId: resource.id, + resourceType: resource.type, + decision: action.decision, + })); + + if (action.decision === "noop") return; + if (opts.dryRun) return; + + if (action.decision === "create" || action.decision === "drift-recreate") { + const primaryKey = yield* runProviderCall( + step, + `${prefix}:create`, + () => resource.create(params), + resource, + opts.retryOptions, + ); + resource.setOutput(params); + if (primaryKey) resource.setOutput({ ...primaryKey, ...resource.output }); + } else { + if (!resource.update) return; + yield* runProviderCall( + step, + `${prefix}:update`, + () => + resource.update!( + resource.key, + action.patch, + params, + resource.toState(resource.output), + ), + resource, + opts.retryOptions, + ); + resource.setOutput({ ...resource.key, ...params }); + } + + const read = yield* readRemote( + step, + resource, + opts, + `${prefix}:read-after-write`, + ); + if (read.status === "found") + resource.setOutput({ ...resource.output, ...read.output }); + if (opts.dryRun) return; + + const operation = + action.decision === "create" || action.decision === "drift-recreate" + ? "create" + : "update"; + const nextState: StoredResourceState = { + id: resource.id, + groupId: resource.groupId, + groupType: resource.groupType, + type: resource.type, + lastOperation: operation, + lastOperationAt: new Date().toISOString(), + config: resource.config, + params: resource.toState(params), + output: resource.toState(resource.output), + }; + + if (!stateStore || !snapshot) { + yield* step.store(yieldStarResourceStateStore, { + id: opts.state.storeId(resource.id), + initial: nextState, + }); + return; + } + + const result = yield* stateStore.updateFrom( + `${prefix}:state:persist`, + snapshot, + () => nextState, + ); + if (!result.updated) + throw new RevConflict( + resource.id, + stateNode?.rev ?? 0, + result.actualVersion + 1, + ); +} + +async function* deleteResource( + step: YieldStarStep, + resource: BaseResource, + opts: YieldStarReconciliationOptions, + suffix: string, +): AsyncGenerator { + const prefix = `notation:${suffix}:${resource.id}`; + const stateStore = yield* openResourceState(step, opts.state, resource.id); + const snapshot = yield* stateStore.get(`${prefix}:state:get`); + const stateNode = toStateNode(snapshot); + resource.setOutput(stateNode.output); + + if (!opts.dryRun) { + try { + yield* runProviderCall( + step, + `${prefix}:delete`, + () => resource.delete(resource.key, resource.toState(resource.output)), + resource, + opts.retryOptions, + ); + } catch (error) { + if (!matchError(error, resource.notFoundOnError)) throw error; + } + + const deleted = yield* stateStore.deleteFrom( + `${prefix}:state:delete`, + snapshot, + ); + if (!deleted.deleted) + throw new RevConflict(resource.id, stateNode.rev, undefined); + } +} + +async function* readRemote( + step: YieldStarStep, + resource: BaseResource, + opts: YieldStarReconciliationOptions, + key: string, +): AsyncGenerator< + any, + | { status: "found"; output: Record } + | { status: "not-found" }, + any +> { + if (!resource.read) { + return { + status: "found", + output: { ...(await resource.getParams()), ...resource.output }, + }; + } + + try { + const output = yield* step.run(key, async () => { + const value = await resource.read!(resource.key); + const unsettled = (resource.retryReadOnCondition ?? []) + .filter(Boolean) + .find((condition) => { + const actual = value[condition!.key]; + return condition!.value === undefined + ? !actual + : actual !== condition!.value; + }); + if (unsettled) { + throw new RetryableError(unsettled.reason, { + ...(opts.readPollOptions ?? DEFAULT_READ_POLL_OPTIONS), + }); + } + return value; + }); + return { status: "found", output }; + } catch (error) { + if (matchError(error, resource.notFoundOnError)) + return { status: "not-found" }; + throw error; + } +} + +function runProviderCall( + step: YieldStarStep, + key: string, + call: () => T | Promise, + resource: BaseResource, + retryOptions?: PollOptions, +) { + return step.run(key, async () => { + try { + return await call(); + } catch (error) { + const matcher = matchError(error, resource.retryLaterOnError); + if (matcher) { + throw new RetryableError(matcher.reason, { + ...(retryOptions ?? DEFAULT_RETRY_OPTIONS), + }); + } + throw error; + } + }); +} + +function openResourceState( + step: YieldStarStep, + state: YieldStarStateBackend, + resourceId: string, +) { + return step.store(yieldStarResourceStateStore, { + id: state.storeId(resourceId), + }); +} + +function emitDurably( + step: YieldStarStep, + key: string, + emit: ReconcilerEventEmitter | undefined, + event: () => Parameters[0], +) { + return step.run(key, async () => { + await emit?.(event()); + }); +} + +/** A Notation state backend backed by YieldStar 0.5 durable stores. */ +export class YieldStarStateBackend { + readonly #client: StoreClient; + readonly #deploymentId: string; + + constructor(client: StoreClient, deploymentId: string) { + this.#client = client; + this.#deploymentId = deploymentId; + } + + storeId(resourceId: string) { + return `${this.#deploymentId}:${resourceId}`; + } + + async get(id: string): Promise { + const storeId = this.storeId(id); + const ids = await this.#client.listStores(yieldStarResourceStateStore); + if (!ids.includes(storeId)) return undefined; + return toStateNode( + await this.#client.getStore({ + definition: yieldStarResourceStateStore, + id: storeId, + }), + ); + } + + async has(id: string): Promise { + return (await this.get(id)) !== undefined; + } + + async update( + id: string, + expectedRev: number, + patch: Partial, + ): Promise<{ rev: number }> { + const storeId = this.storeId(id); + const ids = await this.#client.listStores(yieldStarResourceStateStore); + if (!ids.includes(storeId)) { + if (expectedRev !== 0) throw new RevConflict(id, expectedRev, undefined); + const initial = { ...patch, id } as StoredResourceState; + const created = await this.#client.getOrCreateStore({ + definition: yieldStarResourceStateStore, + id: storeId, + initial, + }); + return { rev: created.version + 1 }; + } + + const snapshot = await this.#client.getStore({ + definition: yieldStarResourceStateStore, + id: storeId, + }); + const actualRev = snapshot.version + 1; + if (actualRev !== expectedRev) + throw new RevConflict(id, expectedRev, actualRev); + const rev = actualRev + 1; + const result = await this.#client.updateStoreFrom({ + definition: yieldStarResourceStateStore, + id: storeId, + snapshot, + updater: (draft) => { + Object.assign(draft, withoutRev(patch)); + }, + }); + if (!result.updated) throw new RevConflict(id, expectedRev, undefined); + return { rev }; + } + + async delete(id: string, expectedRev: number): Promise { + const storeId = this.storeId(id); + const ids = await this.#client.listStores(yieldStarResourceStateStore); + if (!ids.includes(storeId)) { + if (expectedRev !== 0) throw new RevConflict(id, expectedRev, undefined); + return; + } + const snapshot = await this.#client.getStore({ + definition: yieldStarResourceStateStore, + id: storeId, + }); + const actualRev = snapshot.version + 1; + if (actualRev !== expectedRev) + throw new RevConflict(id, expectedRev, actualRev); + const result = await this.#client.deleteStoreFrom({ + definition: yieldStarResourceStateStore, + id: storeId, + snapshot, + }); + if (!result.deleted) throw new RevConflict(id, expectedRev, undefined); + } + + async values(): Promise { + const prefix = `${this.#deploymentId}:`; + const ids = await this.#client.listStores(yieldStarResourceStateStore); + const nodes = await Promise.all( + ids + .filter((id) => id.startsWith(prefix)) + .map(async (id) => + toStateNode( + await this.#client.getStore({ + definition: yieldStarResourceStateStore, + id, + }), + ), + ), + ); + return nodes; + } + + snapshot(id: string) { + return this.#client.getStore({ + definition: yieldStarResourceStateStore, + id: this.storeId(id), + }); + } + + async clear(): Promise { + const prefix = `${this.#deploymentId}:`; + const ids = await this.#client.listStores(yieldStarResourceStateStore); + await Promise.all( + ids + .filter((id) => id.startsWith(prefix)) + .map((id) => + this.#client.deleteStore({ + definition: yieldStarResourceStateStore, + id, + }), + ), + ); + } +} + +function toStateNode(snapshot: { + state: StoredResourceState; + version: number; +}): StateNode { + return { ...snapshot.state, rev: snapshot.version + 1 } as StateNode; +} + +function withoutRev(patch: Partial): Partial { + const { rev: _rev, ...stored } = patch; + return stored; +} + +function plainObjectSchema>( + label: string, + refine: (value: Record) => boolean, +): StandardSchemaV1 { + return { + "~standard": { + version: 1, + vendor: "notation", + validate(value) { + if (!isPlainObject(value) || !refine(value)) { + return { issues: [{ message: `${label} is invalid` }] }; + } + return { value: value as T }; + }, + }, + }; +} + +function isPlainObject(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} diff --git a/packages/reconciler/test/yieldstar.integration.test.ts b/packages/reconciler/test/yieldstar.integration.test.ts new file mode 100644 index 0000000..0ddbc5d --- /dev/null +++ b/packages/reconciler/test/yieldstar.integration.test.ts @@ -0,0 +1,272 @@ +import { + WorkflowRunner, + type HeapClient, + type WorkflowEvent, +} from "@yieldstar/core"; +import { + SqliteHeapClient, + SqliteStoreClient, + createSqliteDb, +} from "@yieldstar/sqlite-runtime/node"; +import { resource, type BaseResource } from "@notation/resource"; +import pino from "pino"; +import { createWorkflowRouter, workflow } from "yieldstar"; +import { describe, expect, it, vi } from "vitest"; +import { + YieldStarStateBackend, + reconcileWithYieldStar, + yieldStarResourceStateStore, +} from "../src/yieldstar"; + +const logger = pino({ level: "silent" }); + +describe("YieldStar reconciliation", () => { + it("waits durably for a retryable provider and persists after success", async () => { + let attempts = 0; + const PendingResource = resource({ type: "test/yieldstar/pending" }) + .defineSchema({}) + .defineOperations({ + create: async () => { + attempts += 1; + if (attempts === 1) { + const error = new Error("provider is pending"); + error.name = "ProviderPending"; + throw error; + } + }, + delete: async () => undefined, + retryLaterOnError: [ + { name: "ProviderPending", reason: "provider is pending" }, + ], + }); + const runtime = createRuntime( + [new PendingResource({ id: "pending" })], + "durable-wait", + { maxAttempts: 3, retryInterval: 1 }, + ); + + await runtime.run("wait-execution"); + expect(attempts).toBe(1); + expect(runtime.scheduler.events).toHaveLength(1); + + await runtime.run("wait-execution"); + expect(attempts).toBe(2); + expect(await runtime.state.get("pending")).toMatchObject({ + id: "pending", + lastOperation: "create", + rev: 1, + }); + runtime.close(); + }); + + it("resumes after a crash without repeating a completed create", async () => { + const create = vi.fn(async () => undefined); + const TestResource = resource({ type: "test/yieldstar/resume" }) + .defineSchema({}) + .defineOperations({ create, delete: async () => undefined }); + const runtime = createRuntime( + [new TestResource({ id: "resume" })], + "crash-resume", + undefined, + "notation:resource:resume:create", + ); + + await expect(runtime.run("resume-execution")).rejects.toThrow( + "simulated process crash", + ); + expect(create).toHaveBeenCalledOnce(); + expect(await runtime.state.get("resume")).toBeUndefined(); + + await runtime.run("resume-execution"); + expect(create).toHaveBeenCalledOnce(); + expect(await runtime.state.get("resume")).toMatchObject({ rev: 1 }); + runtime.close(); + }); + + it("uses store identity and version for conditional update and delete", async () => { + const runtime = createRuntime([], "conditional-state"); + await runtime.state.update("resource", 0, statePatch("resource")); + const originalSnapshot = await runtime.state.snapshot("resource"); + + const first = runtime.state.update("resource", 1, { + output: { winner: "first" }, + }); + const second = runtime.state.update("resource", 1, { + output: { winner: "second" }, + }); + const results = await Promise.allSettled([first, second]); + + expect( + results.filter((result) => result.status === "fulfilled"), + ).toHaveLength(1); + expect( + results.filter((result) => result.status === "rejected"), + ).toHaveLength(1); + await expect(runtime.state.delete("resource", 1)).rejects.toMatchObject({ + name: "RevConflict", + }); + expect(await runtime.state.get("resource")).toMatchObject({ rev: 2 }); + + await runtime.state.clear(); + await runtime.state.update("resource", 0, statePatch("resource")); + const staleDelete = await runtime.storeClient.deleteStoreFrom({ + definition: yieldStarResourceStateStore, + id: runtime.state.storeId("resource"), + snapshot: originalSnapshot, + }); + expect(staleDelete).toMatchObject({ + deleted: false, + reason: "conflict", + }); + expect(await runtime.state.get("resource")).toMatchObject({ rev: 1 }); + runtime.close(); + }); + + it("serializes concurrent deployments through durable store waiting", async () => { + let unblockCreate!: () => void; + const blocked = new Promise((resolve) => { + unblockCreate = resolve; + }); + let started!: () => void; + const createStarted = new Promise((resolve) => { + started = resolve; + }); + const create = vi.fn(async () => { + started(); + await blocked; + }); + const TestResource = resource({ type: "test/yieldstar/concurrent" }) + .defineSchema({}) + .defineOperations({ create, delete: async () => undefined }); + const runtime = createRuntime( + [new TestResource({ id: "shared" })], + "concurrent", + ); + + const first = runtime.run("deployment-a"); + await createStarted; + await runtime.run("deployment-b"); + expect(create).toHaveBeenCalledOnce(); + + unblockCreate(); + await first; + const wake = runtime.scheduler.events.find( + (event) => event.executionId === "deployment-b", + ); + expect(wake).toBeDefined(); + await runtime.runner.run(wake!, logger); + + expect(create).toHaveBeenCalledOnce(); + expect(await runtime.state.values()).toHaveLength(1); + runtime.close(); + }); +}); + +function createRuntime( + resources: BaseResource[], + deploymentId: string, + retryOptions?: { maxAttempts: number; retryInterval: number }, + crashAfterStep?: string, +) { + const database = createSqliteDb({ path: ":memory:" }); + const scheduler = new TestScheduler(); + const sqliteHeap = new SqliteHeapClient(database); + const heap = crashAfterStep + ? new CrashAfterWriteHeap(sqliteHeap, crashAfterStep) + : sqliteHeap; + const storeClient = new SqliteStoreClient({ + db: database, + schedulerClient: scheduler, + }); + const state = new YieldStarStateBackend(storeClient, deploymentId); + const deploy = workflow(async function* (step, event) { + yield* reconcileWithYieldStar(step, { + deploymentId, + executionId: event.executionId, + resources, + state, + driftDetection: false, + retryOptions, + }); + }); + const router = createWorkflowRouter({ deploy }); + const runner = new WorkflowRunner({ + router, + heapClient: heap, + storeClient, + schedulerClient: scheduler, + logger, + }); + + return { + runner, + scheduler, + state, + storeClient, + run(executionId: string) { + return runner.run( + { + workflowId: "deploy", + executionId, + params: {}, + context: new Map(), + }, + logger, + ); + }, + close() { + database.close(); + }, + }; +} + +class TestScheduler { + readonly events: WorkflowEvent[] = []; + + async requestWakeUp(event: WorkflowEvent) { + this.events.push(event); + } +} + +class CrashAfterWriteHeap implements HeapClient { + #crashed = false; + + constructor( + private readonly inner: HeapClient, + private readonly crashAfterStep: string, + ) {} + + readStep(params: { executionId: string; stepKey: string }) { + return this.inner.readStep(params); + } + + async writeStep(params: { + executionId: string; + stepKey: string; + stepAttempt: number; + stepDone: boolean; + stepResponseJson: string; + }) { + await this.inner.writeStep(params); + if ( + !this.#crashed && + params.stepKey === this.crashAfterStep && + params.stepDone + ) { + this.#crashed = true; + throw new Error("simulated process crash"); + } + } +} + +function statePatch(id: string) { + return { + id, + type: "test/yieldstar/state", + config: {}, + params: {}, + output: {}, + lastOperation: "create" as const, + lastOperationAt: new Date().toISOString(), + }; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0b23b2f..7621ec9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -123,9 +123,18 @@ importers: '@notation/resource': specifier: workspace:* version: link:../../packages/resource - '@notation/state-sqlite': - specifier: workspace:* - version: link:../../packages/state-sqlite + '@yieldstar/core': + specifier: 0.5.0 + version: 0.5.0 + '@yieldstar/sqlite-runtime': + specifier: 0.5.0 + version: 0.5.0 + pino: + specifier: ^9.9.0 + version: 9.14.0 + yieldstar: + specifier: 0.5.0 + version: 0.5.0 devDependencies: '@types/node': specifier: ^22.13.4 @@ -342,12 +351,22 @@ importers: '@notation/state': specifier: workspace:* version: link:../state + '@yieldstar/core': + specifier: 0.5.0 + version: 0.5.0 deep-object-diff: specifier: ^1.1.9 version: 1.1.9 yieldstar: - specifier: ^0.4.6 - version: 0.4.6 + specifier: 0.5.0 + version: 0.5.0 + devDependencies: + '@yieldstar/sqlite-runtime': + specifier: 0.5.0 + version: 0.5.0 + pino: + specifier: ^9.9.0 + version: 9.14.0 packages/resource: {} @@ -1524,8 +1543,12 @@ packages: '@vitest/utils@4.1.10': resolution: {integrity: sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==} - '@yieldstar/core@0.4.6': - resolution: {integrity: sha512-6aJQ2NwA07YKdQpiyxdg+SCKwnHQsuxAGGW6uKZYydTS10ljL3uTE6mKdrt/22ehOtbXzjRa/xnECAWh3GM7pw==} + '@yieldstar/core@0.5.0': + resolution: {integrity: sha512-KaN1+AVg54W9G4VXHNmCRixV0MY123714YZgaA9EZx7mDgB0vfFA6T71rUnldGH86QEkocCwI4GCZzZ+0G4fyQ==} + + '@yieldstar/sqlite-runtime@0.5.0': + resolution: {integrity: sha512-zdG4kvOzEJFrTW52SY+WtdUuSvKhXs438H2/+TQotwi3fltLqva9EMgRJ/l9mcvC7nqAq1i0g2v+TZWjO/py4A==} + engines: {node: '>=22.6'} abstract-logging@2.0.1: resolution: {integrity: sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==} @@ -2209,19 +2232,9 @@ packages: pino-abstract-transport@2.0.0: resolution: {integrity: sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==} - pino-abstract-transport@3.0.0: - resolution: {integrity: sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==} - - pino-std-serializers@7.0.0: - resolution: {integrity: sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==} - pino-std-serializers@7.1.0: resolution: {integrity: sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==} - pino@10.3.1: - resolution: {integrity: sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==} - hasBin: true - pino@9.14.0: resolution: {integrity: sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w==} hasBin: true @@ -2310,9 +2323,6 @@ packages: resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} engines: {node: '>= 12.13.0'} - real-require@1.0.0: - resolution: {integrity: sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g==} - require-from-string@2.0.2: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} @@ -2405,9 +2415,6 @@ packages: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} - sonic-boom@4.2.0: - resolution: {integrity: sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==} - sonic-boom@4.2.1: resolution: {integrity: sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==} @@ -2477,10 +2484,6 @@ packages: thread-stream@3.1.0: resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==} - thread-stream@4.2.0: - resolution: {integrity: sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ==} - engines: {node: '>=20'} - tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} @@ -2584,6 +2587,10 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + uuid@11.1.1: + resolution: {integrity: sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==} + hasBin: true + vite@8.1.3: resolution: {integrity: sha512-Ds+gBRbj0lwRO2Y5hwnUBdxSwlAve9LeRyU4sNnAr0ewW0gWF0n5bgXgUzbgZ49MV9BVUAQUFYVcDUcilUExMA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2685,8 +2692,8 @@ packages: yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} - yieldstar@0.4.6: - resolution: {integrity: sha512-toVxX0hHx+AlD5S3MT7voOW/Jw7AX5Mqejp28BJErSHGdZVcuKyVIfzYknbfnRnmBxAeZd3wfVnk7JQJQzZUZQ==} + yieldstar@0.5.0: + resolution: {integrity: sha512-MKuo2uaHYdy+1u0O0Q3Po3eVLGEmiNACsbaQ9aqOD4miZyRT63fXc/mhk0V4n8fNtoMN1BORD9QiXq5ZuOBtkQ==} yoctocolors@2.1.1: resolution: {integrity: sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ==} @@ -3819,7 +3826,15 @@ snapshots: convert-source-map: 2.0.0 tinyrainbow: 3.1.0 - '@yieldstar/core@0.4.6': {} + '@yieldstar/core@0.5.0': + dependencies: + '@standard-schema/spec': 1.1.0 + + '@yieldstar/sqlite-runtime@0.5.0': + dependencies: + '@yieldstar/core': 0.5.0 + pino: 9.14.0 + uuid: 11.1.1 abstract-logging@2.0.1: {} @@ -4092,7 +4107,7 @@ snapshots: fast-json-stringify: 7.0.0 find-my-way: 9.6.0 light-my-request: 6.6.0 - pino: 10.3.1 + pino: 9.14.0 process-warning: 5.0.0 rfdc: 1.4.1 secure-json-parse: 4.1.0 @@ -4441,40 +4456,20 @@ snapshots: dependencies: split2: 4.2.0 - pino-abstract-transport@3.0.0: - dependencies: - split2: 4.2.0 - - pino-std-serializers@7.0.0: {} - pino-std-serializers@7.1.0: {} - pino@10.3.1: - dependencies: - '@pinojs/redact': 0.4.0 - atomic-sleep: 1.0.0 - on-exit-leak-free: 2.1.2 - pino-abstract-transport: 3.0.0 - pino-std-serializers: 7.1.0 - process-warning: 5.0.0 - quick-format-unescaped: 4.0.4 - real-require: 0.2.0 - safe-stable-stringify: 2.5.0 - sonic-boom: 4.2.1 - thread-stream: 4.2.0 - pino@9.14.0: dependencies: '@pinojs/redact': 0.4.0 atomic-sleep: 1.0.0 on-exit-leak-free: 2.1.2 pino-abstract-transport: 2.0.0 - pino-std-serializers: 7.0.0 + pino-std-serializers: 7.1.0 process-warning: 5.0.0 quick-format-unescaped: 4.0.4 real-require: 0.2.0 safe-stable-stringify: 2.5.0 - sonic-boom: 4.2.0 + sonic-boom: 4.2.1 thread-stream: 3.1.0 pirates@4.0.7: {} @@ -4539,8 +4534,6 @@ snapshots: real-require@0.2.0: {} - real-require@1.0.0: {} - require-from-string@2.0.2: {} resolve-from@5.0.0: {} @@ -4645,10 +4638,6 @@ snapshots: slash@3.0.0: {} - sonic-boom@4.2.0: - dependencies: - atomic-sleep: 1.0.0 - sonic-boom@4.2.1: dependencies: atomic-sleep: 1.0.0 @@ -4708,10 +4697,6 @@ snapshots: dependencies: real-require: 0.2.0 - thread-stream@4.2.0: - dependencies: - real-require: 1.0.0 - tinybench@2.9.0: {} tinyexec@0.3.2: {} @@ -4800,6 +4785,8 @@ snapshots: util-deprecate@1.0.2: {} + uuid@11.1.1: {} + vite@8.1.3(@types/node@22.13.4)(esbuild@0.28.1)(jiti@2.7.0): dependencies: lightningcss: 1.32.0 @@ -4853,9 +4840,9 @@ snapshots: yallist@3.1.1: {} - yieldstar@0.4.6: + yieldstar@0.5.0: dependencies: - '@yieldstar/core': 0.4.6 + '@yieldstar/core': 0.5.0 nanoid: 5.1.16 pino: 9.14.0 serialize-error: 11.0.3 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 8d0e4c7..3c6d254 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -3,6 +3,10 @@ packages: - "packages/*" allowBuilds: esbuild: true +minimumReleaseAgeExclude: + - '@yieldstar/core@0.5.0' + - '@yieldstar/sqlite-runtime@0.5.0' + - yieldstar@0.5.0 overrides: lodash-es@<4.18.1: ^4.18.1 js-yaml@3: 3.15.0