Server-side scenario execution: thin effects client replaces local engine

- engine/ (DagWalker, sessions, IndexedDbPlanStore) deleted; server owns the graph
- trigger/callback/counter carry PendingEffect; GET /sdk/v1/scenarios/pending polled
  (30s jittered heartbeat paused on hidden tab + wait-deadline timers)
- client.effects public API unchanged (done/buy/dismiss/end/claim/battlepass/quest)
- planStateStore removed; loginEvent kept; reconcile() makes server the source of truth
- generated models regenerated; skills/README/CHANGELOG updated
This commit is contained in:
edmand46
2026-09-04 14:08:48 +03:00
parent ecbbf93951
commit 1491b5e357
33 changed files with 976 additions and 2050 deletions
+14 -70
View File
@@ -1,72 +1,16 @@
import type { ExecutionPlan, ExecutionPlanNode, PlanEdge, BoundaryNode } from '../../src/generated/common.js';
import type { PendingEffect } from '../../src/generated/scenarios.js';
/**
* Small fluent builder for ExecutionPlans, so scenario journeys read like the
* DAGs they model rather than walls of object literals.
*
* plan('offer_flow')
* .node('wait1', 'wait', { duration: 1, unit: 'minutes' })
* .node('offer', 'store', { storeSlug: 'starter', offerId: 'pack_1' })
* .edge('wait1', 'onComplete', 'offer')
* .build();
*
* The first node added becomes the start node unless `.start(id)` is called.
*/
export class PlanBuilder {
private readonly nodes: ExecutionPlanNode[] = [];
private readonly edges: PlanEdge[] = [];
private readonly boundaryNodes: BoundaryNode[] = [];
private startNodeId?: string;
private runIdValue?: string;
constructor(
private readonly scenarioId: string,
private readonly opts: { planId?: string; userId?: string } = {},
) {}
/** Sets a specific run ID (default: auto-derived from scenarioId). */
runId(id: string): this {
this.runIdValue = id;
return this;
}
node(id: string, type: string, data?: Record<string, unknown>): this {
this.nodes.push({ id, type, data: data as ExecutionPlanNode['data'] });
if (!this.startNodeId) this.startNodeId = id;
return this;
}
edge(source: string, sourceHandle: string, target: string): this {
this.edges.push({ id: `e-${source}-${sourceHandle}-${target}`, source, sourceHandle, target });
return this;
}
/** Registers a server-side boundary node (handle that calls back to the gateway). */
boundary(sourceNodeId: string, sourceHandle: string, nodeId = `b-${sourceNodeId}-${sourceHandle}`): this {
this.boundaryNodes.push({ sourceNodeId, sourceHandle, nodeId });
return this;
}
start(id: string): this {
this.startNodeId = id;
return this;
}
build(): ExecutionPlan {
return {
planId: this.opts.planId ?? `${this.scenarioId}-plan`,
scenarioId: this.scenarioId,
userId: this.opts.userId ?? 'player-1',
startNodeId: this.startNodeId,
runId: this.runIdValue ?? `${this.scenarioId}-run`,
nodes: this.nodes,
edges: this.edges,
boundaryNodes: this.boundaryNodes,
context: undefined,
};
}
}
export function plan(scenarioId: string, opts?: { planId?: string; userId?: string }): PlanBuilder {
return new PlanBuilder(scenarioId, opts);
export function effect(
type: string,
data: Record<string, unknown> = {},
overrides: Partial<PendingEffect> = {},
): PendingEffect {
return {
runId: 'run-1',
scenarioId: 'scenario-1',
nodeId: `${type}-1`,
type,
data,
...overrides,
};
}