spexcode 0.6.6 → 0.6.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -7
- package/node_modules/@spexcode/session-application/dist/index.d.ts +17 -0
- package/node_modules/@spexcode/session-application/dist/index.js +35 -0
- package/node_modules/@spexcode/session-application/dist/migration.d.ts +51 -0
- package/node_modules/@spexcode/session-application/dist/migration.js +694 -0
- package/node_modules/@spexcode/session-application/dist/production.d.ts +109 -0
- package/node_modules/@spexcode/session-application/dist/production.js +438 -0
- package/node_modules/@spexcode/session-application/dist/schema.d.ts +2 -0
- package/node_modules/@spexcode/session-application/dist/schema.js +39 -0
- package/node_modules/@spexcode/session-application/package.json +29 -0
- package/node_modules/@spexcode/session-events/dist/errors.d.ts +6 -0
- package/node_modules/@spexcode/session-events/dist/errors.js +11 -0
- package/node_modules/@spexcode/session-events/dist/index.d.ts +46 -0
- package/node_modules/@spexcode/session-events/dist/index.js +245 -0
- package/node_modules/@spexcode/session-events/dist/schema.d.ts +3 -0
- package/node_modules/@spexcode/session-events/dist/schema.js +44 -0
- package/node_modules/@spexcode/session-events/package.json +24 -0
- package/node_modules/@spexcode/session-protocol/dist/canonical.d.ts +36 -0
- package/node_modules/@spexcode/session-protocol/dist/canonical.js +139 -0
- package/node_modules/@spexcode/session-protocol/dist/engine.d.ts +22 -0
- package/node_modules/@spexcode/session-protocol/dist/engine.js +478 -0
- package/node_modules/@spexcode/session-protocol/dist/errors.d.ts +8 -0
- package/node_modules/@spexcode/session-protocol/dist/errors.js +39 -0
- package/node_modules/@spexcode/session-protocol/dist/index.d.ts +60 -0
- package/node_modules/@spexcode/session-protocol/dist/index.js +4 -0
- package/node_modules/@spexcode/session-protocol/dist/schema.d.ts +36 -0
- package/node_modules/@spexcode/session-protocol/dist/schema.js +192 -0
- package/node_modules/@spexcode/{session-core → session-protocol}/package.json +4 -8
- package/node_modules/@spexcode/session-runtime/dist/errors.d.ts +6 -0
- package/node_modules/@spexcode/session-runtime/dist/errors.js +11 -0
- package/node_modules/@spexcode/session-runtime/dist/index.d.ts +33 -0
- package/node_modules/@spexcode/session-runtime/dist/index.js +193 -0
- package/node_modules/@spexcode/session-runtime/dist/schema.d.ts +2 -0
- package/node_modules/@spexcode/session-runtime/dist/schema.js +37 -0
- package/node_modules/@spexcode/session-runtime/package.json +24 -0
- package/node_modules/@spexcode/session-selflaunch/bin/spex-session.mjs +4 -0
- package/node_modules/@spexcode/session-selflaunch/dist/cli.d.ts +25 -0
- package/node_modules/@spexcode/session-selflaunch/dist/cli.js +156 -0
- package/node_modules/@spexcode/session-selflaunch/dist/index.d.ts +28 -0
- package/node_modules/@spexcode/session-selflaunch/dist/index.js +25 -0
- package/node_modules/@spexcode/session-selflaunch/dist/locality.d.ts +20 -0
- package/node_modules/@spexcode/session-selflaunch/dist/locality.js +83 -0
- package/node_modules/@spexcode/session-selflaunch/dist/path.d.ts +13 -0
- package/node_modules/@spexcode/session-selflaunch/dist/path.js +45 -0
- package/node_modules/@spexcode/session-selflaunch/package.json +35 -0
- package/node_modules/@spexcode/session-topology/dist/errors.d.ts +6 -0
- package/node_modules/@spexcode/session-topology/dist/errors.js +11 -0
- package/node_modules/@spexcode/session-topology/dist/index.d.ts +24 -0
- package/node_modules/@spexcode/session-topology/dist/index.js +173 -0
- package/node_modules/@spexcode/session-topology/dist/schema.d.ts +3 -0
- package/node_modules/@spexcode/session-topology/dist/schema.js +27 -0
- package/node_modules/@spexcode/session-topology/package.json +24 -0
- package/node_modules/@spexcode/spec-cli/bin/spex.mjs +56 -19
- package/node_modules/@spexcode/spec-cli/dist/cli.js +102 -59
- package/node_modules/@spexcode/spec-cli/dist/client.d.ts +1 -3
- package/node_modules/@spexcode/spec-cli/dist/client.js +49 -30
- package/node_modules/@spexcode/spec-cli/dist/codex-runtime-generations.d.ts +11 -0
- package/node_modules/@spexcode/spec-cli/dist/codex-runtime-generations.js +46 -9
- package/node_modules/@spexcode/spec-cli/dist/delivery-lock.d.ts +2 -0
- package/node_modules/@spexcode/spec-cli/dist/delivery-lock.js +58 -0
- package/node_modules/@spexcode/spec-cli/dist/doctor.js +53 -11
- package/node_modules/@spexcode/spec-cli/dist/execution-trace.d.ts +1 -0
- package/node_modules/@spexcode/spec-cli/dist/execution-trace.js +2 -2
- package/node_modules/@spexcode/spec-cli/dist/gateway-hub.js +2 -1
- package/node_modules/@spexcode/spec-cli/dist/gateway.js +6 -3
- package/node_modules/@spexcode/spec-cli/dist/graphCache.js +32 -2
- package/node_modules/@spexcode/spec-cli/dist/graphSnapshot.js +57 -2
- package/node_modules/@spexcode/spec-cli/dist/graphStream.d.ts +2 -0
- package/node_modules/@spexcode/spec-cli/dist/graphStream.js +83 -3
- package/node_modules/@spexcode/spec-cli/dist/guide.js +20 -7
- package/node_modules/@spexcode/spec-cli/dist/harness-select.js +16 -3
- package/node_modules/@spexcode/spec-cli/dist/harness.d.ts +15 -3
- package/node_modules/@spexcode/spec-cli/dist/harness.js +331 -50
- package/node_modules/@spexcode/spec-cli/dist/help.js +11 -8
- package/node_modules/@spexcode/spec-cli/dist/hook-prompts.js +8 -0
- package/node_modules/@spexcode/spec-cli/dist/host-resources.js +29 -8
- package/node_modules/@spexcode/spec-cli/dist/host.d.ts +7 -0
- package/node_modules/@spexcode/spec-cli/dist/host.js +93 -0
- package/node_modules/@spexcode/spec-cli/dist/index.js +324 -22
- package/node_modules/@spexcode/spec-cli/dist/init.js +1 -1
- package/node_modules/@spexcode/spec-cli/dist/lint.js +70 -35
- package/node_modules/@spexcode/spec-cli/dist/listen.d.ts +3 -2
- package/node_modules/@spexcode/spec-cli/dist/listen.js +14 -2
- package/node_modules/@spexcode/spec-cli/dist/machine-peer.js +1 -1
- package/node_modules/@spexcode/spec-cli/dist/materialize.d.ts +2 -2
- package/node_modules/@spexcode/spec-cli/dist/materialize.js +176 -35
- package/node_modules/@spexcode/spec-cli/dist/pty-bridge.js +14 -14
- package/node_modules/@spexcode/spec-cli/dist/reviews.js +12 -7
- package/node_modules/@spexcode/spec-cli/dist/runtime-ownership.d.ts +11 -0
- package/node_modules/@spexcode/spec-cli/dist/runtime-ownership.js +79 -1
- package/node_modules/@spexcode/spec-cli/dist/session-application.d.ts +23 -0
- package/node_modules/@spexcode/spec-cli/dist/session-application.js +189 -0
- package/node_modules/@spexcode/spec-cli/dist/session-declarations.js +13 -1
- package/node_modules/@spexcode/spec-cli/dist/session-files.d.ts +6 -0
- package/node_modules/@spexcode/spec-cli/dist/session-files.js +13 -1
- package/node_modules/@spexcode/spec-cli/dist/session-follow.js +39 -22
- package/node_modules/@spexcode/{session-core/dist/record-lock.d.ts → spec-cli/dist/session-record-lock.d.ts} +0 -4
- package/node_modules/@spexcode/spec-cli/dist/session-record-lock.js +94 -0
- package/node_modules/@spexcode/spec-cli/dist/session-runtime-adapter.d.ts +44 -0
- package/node_modules/@spexcode/spec-cli/dist/session-runtime-adapter.js +37 -0
- package/node_modules/@spexcode/spec-cli/dist/session-timeline.d.ts +25 -2
- package/node_modules/@spexcode/spec-cli/dist/session-timeline.js +68 -11
- package/node_modules/@spexcode/spec-cli/dist/session-web.js +4 -4
- package/node_modules/@spexcode/spec-cli/dist/sessions.d.ts +108 -15
- package/node_modules/@spexcode/spec-cli/dist/sessions.js +1465 -744
- package/node_modules/@spexcode/spec-cli/dist/source-list.d.ts +13 -0
- package/node_modules/@spexcode/spec-cli/dist/source-list.js +99 -0
- package/node_modules/@spexcode/spec-cli/dist/source-read.d.ts +16 -0
- package/node_modules/@spexcode/spec-cli/dist/source-read.js +84 -0
- package/node_modules/@spexcode/spec-cli/dist/spec-attachments.d.ts +7 -0
- package/node_modules/@spexcode/spec-cli/dist/spec-attachments.js +89 -0
- package/node_modules/@spexcode/spec-cli/dist/spec-body-edit.d.ts +23 -0
- package/node_modules/@spexcode/spec-cli/dist/spec-body-edit.js +138 -0
- package/node_modules/@spexcode/spec-cli/dist/supervise.js +15 -6
- package/node_modules/@spexcode/spec-cli/dist/transcript-reader.d.ts +36 -0
- package/node_modules/@spexcode/spec-cli/dist/transcript-reader.js +251 -0
- package/node_modules/@spexcode/spec-cli/hooks/dispatch.sh +19 -31
- package/node_modules/@spexcode/spec-cli/hooks/harness.sh +6 -6
- package/node_modules/@spexcode/spec-cli/package.json +6 -6
- package/node_modules/@spexcode/spec-cli/templates/hooks/post-checkout +4 -2
- package/node_modules/@spexcode/spec-cli/templates/hooks/post-merge +2 -1
- package/node_modules/@spexcode/spec-cli/templates/hooks/pre-commit +5 -3
- package/node_modules/@spexcode/spec-cli/templates/hooks/reference-transaction +5 -3
- package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/commands/spec.md +2 -7
- package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/idle/idle.sh +4 -10
- package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/idle/spec.md +1 -1
- package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/mark-active/mark-active.sh +22 -24
- package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/mark-active/spec.md +10 -2
- package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/session-fail/fail.sh +8 -7
- package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/session-fail/spec.md +3 -1
- package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/session-listen/session-listen.sh +133 -0
- package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/session-listen/spec.md +36 -0
- package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/spec.md +2 -0
- package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/stop-gate/spec.md +1 -1
- package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/stop-gate/stop-gate.sh +17 -20
- package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/skills/merge/spec.md +33 -0
- package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/skills/spec.md +2 -6
- package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/spec.md +7 -0
- package/node_modules/@spexcode/spec-core/dist/anchors.d.ts +13 -2
- package/node_modules/@spexcode/spec-core/dist/anchors.js +311 -15
- package/node_modules/@spexcode/spec-core/dist/git.d.ts +4 -0
- package/node_modules/@spexcode/spec-core/dist/git.js +9 -9
- package/node_modules/@spexcode/spec-core/dist/layout.d.ts +12 -0
- package/node_modules/@spexcode/spec-core/dist/layout.js +4 -2
- package/node_modules/@spexcode/spec-core/dist/resilience.d.ts +1 -0
- package/node_modules/@spexcode/spec-core/dist/resilience.js +8 -0
- package/node_modules/@spexcode/spec-core/dist/review/reviewFilters.d.ts +1 -0
- package/node_modules/@spexcode/spec-core/dist/review/reviewFilters.js +7 -3
- package/node_modules/@spexcode/spec-core/dist/review/reviewQuery.d.ts +1 -0
- package/node_modules/@spexcode/spec-core/dist/review/reviewQuery.js +4 -0
- package/node_modules/@spexcode/spec-core/dist/specs.d.ts +2 -0
- package/node_modules/@spexcode/spec-core/dist/specs.js +30 -0
- package/node_modules/@spexcode/spec-core/package.json +4 -1
- package/node_modules/@spexcode/spec-eval/dist/cli.js +1 -1
- package/node_modules/@spexcode/spec-eval/dist/evaltab.d.ts +1 -0
- package/node_modules/@spexcode/spec-eval/dist/evaltab.js +6 -1
- package/node_modules/@spexcode/spec-eval/dist/freshness.d.ts +1 -1
- package/node_modules/@spexcode/spec-eval/dist/freshness.js +12 -12
- package/node_modules/@spexcode/spec-eval/dist/host.d.ts +2 -1
- package/node_modules/@spexcode/spec-eval/dist/sessioneval.d.ts +2 -1
- package/node_modules/@spexcode/spec-eval/dist/sessioneval.js +20 -3
- package/node_modules/@spexcode/spec-eval/package.json +2 -2
- package/node_modules/@spexcode/spec-forge/package.json +2 -2
- package/node_modules/@vscode/tree-sitter-wasm/LICENSE +21 -0
- package/node_modules/@vscode/tree-sitter-wasm/README.md +36 -0
- package/node_modules/@vscode/tree-sitter-wasm/SECURITY.md +41 -0
- package/node_modules/@vscode/tree-sitter-wasm/cgmanifest.json +16 -0
- package/node_modules/@vscode/tree-sitter-wasm/package.json +42 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-bash.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-c-sharp.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-cpp.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-css.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-go.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-ini.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-java.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-javascript.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-php.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-powershell.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-python.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-regex.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-ruby.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-rust.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-tsx.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-typescript.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter.js +4075 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/web-tree-sitter.d.ts +1027 -0
- package/package.json +9 -3
- package/node_modules/@spexcode/session-core/dist/delivery-queue.d.ts +0 -23
- package/node_modules/@spexcode/session-core/dist/delivery-queue.js +0 -195
- package/node_modules/@spexcode/session-core/dist/index.d.ts +0 -5
- package/node_modules/@spexcode/session-core/dist/index.js +0 -5
- package/node_modules/@spexcode/session-core/dist/internal.d.ts +0 -3
- package/node_modules/@spexcode/session-core/dist/internal.js +0 -3
- package/node_modules/@spexcode/session-core/dist/message.d.ts +0 -22
- package/node_modules/@spexcode/session-core/dist/message.js +0 -52
- package/node_modules/@spexcode/session-core/dist/record-lock.js +0 -152
- package/node_modules/@spexcode/session-core/dist/runtime-session.d.ts +0 -50
- package/node_modules/@spexcode/session-core/dist/runtime-session.js +0 -286
- package/node_modules/@spexcode/session-core/dist/session-cursors.d.ts +0 -14
- package/node_modules/@spexcode/session-core/dist/session-cursors.js +0 -82
- package/node_modules/@spexcode/session-core/dist/session-timeline.d.ts +0 -46
- package/node_modules/@spexcode/session-core/dist/session-timeline.js +0 -216
- package/node_modules/@spexcode/spec-cli/hooks/compat/mark-active-0.5.2-eef1.fixture +0 -53
- package/node_modules/@spexcode/spec-cli/hooks/compat/mark-active-sed-v0.fixture +0 -46
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { type Message, type MessageInput, type SessionProtocol } from '@spexcode/session-protocol';
|
|
2
|
+
import { type SessionEvent, type SessionEventStore } from '@spexcode/session-events';
|
|
3
|
+
import { type RuntimeBinding, type SessionRuntimeBindings } from '@spexcode/session-runtime';
|
|
4
|
+
import { type SessionTopology, type TopologyEdge } from '@spexcode/session-topology';
|
|
5
|
+
export interface LocalityPrecondition {
|
|
6
|
+
(databasePath: string): void;
|
|
7
|
+
}
|
|
8
|
+
export interface SessionState {
|
|
9
|
+
sessionId: string;
|
|
10
|
+
status: string;
|
|
11
|
+
proposal: string | null;
|
|
12
|
+
note: string | null;
|
|
13
|
+
parentSessionId: string | null;
|
|
14
|
+
updatedAtMs: number;
|
|
15
|
+
}
|
|
16
|
+
export interface SessionStateChange {
|
|
17
|
+
sessionId: string;
|
|
18
|
+
status: string;
|
|
19
|
+
proposal: string | null;
|
|
20
|
+
note: string | null;
|
|
21
|
+
previousProposal: string | null;
|
|
22
|
+
previousNote: string | null;
|
|
23
|
+
parentSessionId: string | null;
|
|
24
|
+
previousStatus: string | null;
|
|
25
|
+
previousParentSessionId: string | null;
|
|
26
|
+
reason: string | null;
|
|
27
|
+
}
|
|
28
|
+
export interface NativeRuntimeIdentity {
|
|
29
|
+
namespace: string;
|
|
30
|
+
runtimeKind: string;
|
|
31
|
+
nativeSessionId: string;
|
|
32
|
+
nativeStartToken: string;
|
|
33
|
+
metadata?: Record<string, unknown>;
|
|
34
|
+
}
|
|
35
|
+
export interface ProjectSessionApplicationOptions {
|
|
36
|
+
databasePath: string;
|
|
37
|
+
locality: LocalityPrecondition;
|
|
38
|
+
now?: () => number;
|
|
39
|
+
onCommitted?: (result: Pick<CommittedSessionChange, 'recipients'>) => void;
|
|
40
|
+
/** Runtime binding is the event that makes an existing canonical delivery debt drainable. */
|
|
41
|
+
onRuntimeBound?: (sessionId: string) => void;
|
|
42
|
+
}
|
|
43
|
+
export interface CreateSessionInput {
|
|
44
|
+
sessionId: string;
|
|
45
|
+
status?: string;
|
|
46
|
+
parentSessionId?: string | null;
|
|
47
|
+
runtime?: NativeRuntimeIdentity;
|
|
48
|
+
eventId?: string;
|
|
49
|
+
updatedAtMs?: number;
|
|
50
|
+
proposal?: string | null;
|
|
51
|
+
note?: string | null;
|
|
52
|
+
}
|
|
53
|
+
export interface TransitionSessionInput {
|
|
54
|
+
status?: string;
|
|
55
|
+
proposal?: string | null;
|
|
56
|
+
note?: string | null;
|
|
57
|
+
parentSessionId?: string | null;
|
|
58
|
+
reason?: string | null;
|
|
59
|
+
/** An upstream policy may resolve recipients before this neutral transition. */
|
|
60
|
+
recipientSessionIds?: readonly string[];
|
|
61
|
+
}
|
|
62
|
+
export interface CommittedSessionChange {
|
|
63
|
+
state: SessionState;
|
|
64
|
+
event: SessionEvent;
|
|
65
|
+
edge: TopologyEdge | null;
|
|
66
|
+
recipients: string[];
|
|
67
|
+
messages: Message[];
|
|
68
|
+
}
|
|
69
|
+
export interface ConversationMessageInput {
|
|
70
|
+
text: string;
|
|
71
|
+
from: string | null;
|
|
72
|
+
replyVia?: 'note';
|
|
73
|
+
}
|
|
74
|
+
export interface ProductionSessionApplication extends SessionApplication {
|
|
75
|
+
readonly databasePath: string;
|
|
76
|
+
readonly protocol: SessionProtocol;
|
|
77
|
+
readonly topology: SessionTopology;
|
|
78
|
+
readonly events: SessionEventStore;
|
|
79
|
+
readonly runtimeBindings: SessionRuntimeBindings;
|
|
80
|
+
createSession(input: CreateSessionInput): SessionState;
|
|
81
|
+
transitionSession(sessionId: string, input: TransitionSessionInput): CommittedSessionChange;
|
|
82
|
+
enqueueMessage(sessionId: string, message: MessageInput): Message;
|
|
83
|
+
enqueueConversationMessage(sessionId: string, message: MessageInput, conversation: ConversationMessageInput): Message;
|
|
84
|
+
attachWatcher(watcherSessionId: string, subjectSessionId: string, channel?: string): TopologyEdge;
|
|
85
|
+
detachWatcher(watcherSessionId: string, subjectSessionId: string, channel?: string): TopologyEdge;
|
|
86
|
+
listWatchers(watcherSessionId: string, channel?: string): TopologyEdge[];
|
|
87
|
+
bindRuntime(sessionId: string, identity: NativeRuntimeIdentity, expectedGeneration?: number): RuntimeBinding;
|
|
88
|
+
resolveRuntime(sessionId: string, namespace: string): RuntimeBinding | null;
|
|
89
|
+
dequeueForRuntime(sessionId: string, namespace: string, expectedGeneration?: number, expectedMessageId?: string): Message | null;
|
|
90
|
+
readPendingMessages(sessionId: string): readonly Message[];
|
|
91
|
+
readMessageHistory(sessionId: string): readonly Message[];
|
|
92
|
+
dequeuePendingMessage(sessionId: string, expectedMessageId: string): Message | null;
|
|
93
|
+
readState(sessionId: string): SessionState | null;
|
|
94
|
+
readEvents(sessionId: string, afterSequence?: number): readonly SessionEvent[];
|
|
95
|
+
readFollowCursor(watcherSessionId: string, subjectSessionId: string): number | null;
|
|
96
|
+
advanceFollowCursor(watcherSessionId: string, subjectSessionId: string, eventSequence: number): void;
|
|
97
|
+
replayState(sessionId: string): SessionState | null;
|
|
98
|
+
close(): void;
|
|
99
|
+
}
|
|
100
|
+
export interface SessionApplication {
|
|
101
|
+
notifyRecipients(subjectSessionId: string, message: MessageInput): NotificationResult;
|
|
102
|
+
attachAndNotify(fromSessionId: string, subjectSessionId: string, relationType: string, message: MessageInput): NotificationResult;
|
|
103
|
+
}
|
|
104
|
+
export interface NotificationResult {
|
|
105
|
+
edge: TopologyEdge | null;
|
|
106
|
+
recipients: string[];
|
|
107
|
+
messages: Message[];
|
|
108
|
+
}
|
|
109
|
+
export declare function openProjectSessionApplication(options: ProjectSessionApplicationOptions): ProductionSessionApplication;
|
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
import { isAbsolute } from 'node:path';
|
|
3
|
+
import { applyComponentMigrations, openProtocol, } from '@spexcode/session-protocol';
|
|
4
|
+
import { encodeEventJson, openSessionEvents, } from '@spexcode/session-events';
|
|
5
|
+
import { openRuntimeBindings, } from '@spexcode/session-runtime';
|
|
6
|
+
import { openTopology } from '@spexcode/session-topology';
|
|
7
|
+
import { SESSION_APPLICATION_MIGRATIONS } from './schema.js';
|
|
8
|
+
const SESSION_ID = /^(?!-)[0-9A-Za-z_-]{1,256}$/;
|
|
9
|
+
const STATUS = /^[0-9A-Za-z._:-]{1,64}$/;
|
|
10
|
+
const STATE_EVENT = 'session.state.changed.v1';
|
|
11
|
+
const MESSAGE_EVENT = 'session.message.sent.v1';
|
|
12
|
+
const PARENT_RELATION = 'parent';
|
|
13
|
+
const WATCH_RELATION = 'watch';
|
|
14
|
+
const PARENT_WATCH_RELATION = 'watch:parent';
|
|
15
|
+
const MANUAL_WATCH_RELATION = 'watch:manual';
|
|
16
|
+
const compositions = new Map();
|
|
17
|
+
const requirePath = (databasePath) => {
|
|
18
|
+
if (typeof databasePath !== 'string' || !isAbsolute(databasePath) || databasePath.startsWith('file:') || databasePath.includes('\0') || databasePath.endsWith('/')) {
|
|
19
|
+
throw new TypeError('session application requires an explicit absolute databasePath naming a file');
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
const requireId = (value, field) => {
|
|
23
|
+
if (typeof value !== 'string' || !SESSION_ID.test(value))
|
|
24
|
+
throw new TypeError(`${field} must be a valid protocol session id`);
|
|
25
|
+
};
|
|
26
|
+
const requireStatus = (value) => {
|
|
27
|
+
if (typeof value !== 'string' || !STATUS.test(value))
|
|
28
|
+
throw new TypeError('session status has an invalid grammar');
|
|
29
|
+
};
|
|
30
|
+
const eventId = () => randomBytes(16).toString('hex');
|
|
31
|
+
const readStateInTransaction = (tx, sessionId) => {
|
|
32
|
+
const row = tx.query('SELECT session_id, status, proposal, note, parent_session_id, updated_at_ms FROM session_application_state WHERE session_id=?', sessionId)[0];
|
|
33
|
+
return row
|
|
34
|
+
? {
|
|
35
|
+
sessionId: String(row.session_id),
|
|
36
|
+
status: String(row.status),
|
|
37
|
+
proposal: row.proposal === null ? null : String(row.proposal),
|
|
38
|
+
note: row.note === null ? null : String(row.note),
|
|
39
|
+
parentSessionId: row.parent_session_id === null ? null : String(row.parent_session_id),
|
|
40
|
+
updatedAtMs: Number(row.updated_at_ms),
|
|
41
|
+
}
|
|
42
|
+
: null;
|
|
43
|
+
};
|
|
44
|
+
const messageForEvent = (state, id) => ({
|
|
45
|
+
kind: STATE_EVENT,
|
|
46
|
+
body: encodeEventJson({
|
|
47
|
+
eventId: id,
|
|
48
|
+
sessionId: state.sessionId,
|
|
49
|
+
status: state.status,
|
|
50
|
+
proposal: state.proposal,
|
|
51
|
+
note: state.note,
|
|
52
|
+
previousProposal: state.previousProposal,
|
|
53
|
+
previousNote: state.previousNote,
|
|
54
|
+
parentSessionId: state.parentSessionId,
|
|
55
|
+
previousStatus: state.previousStatus,
|
|
56
|
+
previousParentSessionId: state.previousParentSessionId,
|
|
57
|
+
reason: state.reason,
|
|
58
|
+
}),
|
|
59
|
+
senderSessionId: state.sessionId,
|
|
60
|
+
idempotencyKey: id,
|
|
61
|
+
});
|
|
62
|
+
export function openProjectSessionApplication(options) {
|
|
63
|
+
requirePath(options.databasePath);
|
|
64
|
+
if (typeof options.locality !== 'function')
|
|
65
|
+
throw new TypeError('session application requires a locality precondition');
|
|
66
|
+
options.locality(options.databasePath);
|
|
67
|
+
const existing = compositions.get(options.databasePath);
|
|
68
|
+
if (existing)
|
|
69
|
+
return existing;
|
|
70
|
+
const protocol = openProtocolOnce(options.databasePath, options.now);
|
|
71
|
+
applyComponentMigrations(protocol, 'session-application', SESSION_APPLICATION_MIGRATIONS);
|
|
72
|
+
const topology = openTopology(protocol);
|
|
73
|
+
const events = openSessionEvents(protocol);
|
|
74
|
+
const runtimeBindings = openRuntimeBindings(protocol);
|
|
75
|
+
const now = options.now ?? (() => Date.now());
|
|
76
|
+
const initialized = new Set();
|
|
77
|
+
const initialize = (sessionId) => {
|
|
78
|
+
requireId(sessionId, 'sessionId');
|
|
79
|
+
if (!initialized.has(sessionId)) {
|
|
80
|
+
protocol.initialize(sessionId);
|
|
81
|
+
initialized.add(sessionId);
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
const notifyCommitted = (result) => {
|
|
85
|
+
options.onCommitted?.(result);
|
|
86
|
+
};
|
|
87
|
+
// Parent supervision is intentionally quiet for routine active/working transitions. A manual watch
|
|
88
|
+
// opts into the complete feed; when both sources exist, the union keeps that manual choice without
|
|
89
|
+
// enqueueing the same watcher twice. Creation still publishes its initial snapshot through the normal
|
|
90
|
+
// relation recipients; this filter governs later lifecycle transitions only. A queued creation snapshot is
|
|
91
|
+
// corrected once by the first ready active publication, otherwise parent-only supervision would retain the
|
|
92
|
+
// stale queued face forever.
|
|
93
|
+
const lifecycleRecipients = (subjectSessionId, previousStatus, status, tx) => {
|
|
94
|
+
const recipients = new Set();
|
|
95
|
+
for (const edge of topology.parents(subjectSessionId, undefined, tx)) {
|
|
96
|
+
const parentOnly = edge.relationType === PARENT_WATCH_RELATION;
|
|
97
|
+
const manual = edge.relationType === MANUAL_WATCH_RELATION || edge.relationType === WATCH_RELATION;
|
|
98
|
+
if (manual || (parentOnly && (status !== 'active' || previousStatus === 'queued')))
|
|
99
|
+
recipients.add(edge.fromSessionId);
|
|
100
|
+
}
|
|
101
|
+
return [...recipients].sort();
|
|
102
|
+
};
|
|
103
|
+
const app = {
|
|
104
|
+
databasePath: options.databasePath,
|
|
105
|
+
protocol,
|
|
106
|
+
topology,
|
|
107
|
+
events,
|
|
108
|
+
runtimeBindings,
|
|
109
|
+
notifyRecipients(subjectSessionId, message) {
|
|
110
|
+
const result = protocol.withTransaction(tx => {
|
|
111
|
+
const recipients = topology.recipients(subjectSessionId, tx);
|
|
112
|
+
const messages = recipients.map(recipient => tx.enqueue(recipient, {
|
|
113
|
+
...message,
|
|
114
|
+
senderSessionId: message.senderSessionId === undefined ? subjectSessionId : message.senderSessionId,
|
|
115
|
+
}));
|
|
116
|
+
return { edge: null, recipients, messages };
|
|
117
|
+
});
|
|
118
|
+
notifyCommitted(result);
|
|
119
|
+
return result;
|
|
120
|
+
},
|
|
121
|
+
attachAndNotify(fromSessionId, subjectSessionId, relationType, message) {
|
|
122
|
+
const result = protocol.withTransaction(tx => {
|
|
123
|
+
const edge = topology.attach(tx, fromSessionId, subjectSessionId, relationType);
|
|
124
|
+
const recipients = topology.recipients(subjectSessionId, tx);
|
|
125
|
+
const messages = recipients.map(recipient => tx.enqueue(recipient, {
|
|
126
|
+
...message,
|
|
127
|
+
senderSessionId: message.senderSessionId === undefined ? subjectSessionId : message.senderSessionId,
|
|
128
|
+
}));
|
|
129
|
+
return { edge, recipients, messages };
|
|
130
|
+
});
|
|
131
|
+
notifyCommitted(result);
|
|
132
|
+
return result;
|
|
133
|
+
},
|
|
134
|
+
createSession(input) {
|
|
135
|
+
requireId(input.sessionId, 'sessionId');
|
|
136
|
+
const status = input.status ?? 'created';
|
|
137
|
+
requireStatus(status);
|
|
138
|
+
const proposal = input.proposal ?? null;
|
|
139
|
+
const note = input.note ?? null;
|
|
140
|
+
const parentSessionId = input.parentSessionId ?? null;
|
|
141
|
+
if (parentSessionId !== null)
|
|
142
|
+
requireId(parentSessionId, 'parentSessionId');
|
|
143
|
+
initialize(input.sessionId);
|
|
144
|
+
if (parentSessionId)
|
|
145
|
+
initialize(parentSessionId);
|
|
146
|
+
const created = protocol.withTransaction(tx => {
|
|
147
|
+
if (readStateInTransaction(tx, input.sessionId)) {
|
|
148
|
+
throw new Error(`session application state already exists: ${input.sessionId}`);
|
|
149
|
+
}
|
|
150
|
+
let edge = null;
|
|
151
|
+
if (parentSessionId)
|
|
152
|
+
edge = topology.attach(tx, parentSessionId, input.sessionId, PARENT_RELATION);
|
|
153
|
+
const updatedAtMs = input.updatedAtMs ?? now();
|
|
154
|
+
tx.exec('INSERT INTO session_application_state(session_id,status,proposal,note,parent_session_id,updated_at_ms) VALUES(?,?,?,?,?,?)', input.sessionId, status, proposal, note, parentSessionId, updatedAtMs);
|
|
155
|
+
const id = input.eventId ?? eventId();
|
|
156
|
+
const change = {
|
|
157
|
+
sessionId: input.sessionId,
|
|
158
|
+
status,
|
|
159
|
+
proposal,
|
|
160
|
+
note,
|
|
161
|
+
previousProposal: null,
|
|
162
|
+
previousNote: null,
|
|
163
|
+
parentSessionId,
|
|
164
|
+
previousStatus: null,
|
|
165
|
+
previousParentSessionId: null,
|
|
166
|
+
reason: 'create',
|
|
167
|
+
};
|
|
168
|
+
const event = events.append(tx, {
|
|
169
|
+
eventId: id,
|
|
170
|
+
type: STATE_EVENT,
|
|
171
|
+
schemaVersion: 1,
|
|
172
|
+
subjectSessionId: input.sessionId,
|
|
173
|
+
payload: encodeEventJson(change),
|
|
174
|
+
occurredAtMs: updatedAtMs,
|
|
175
|
+
});
|
|
176
|
+
const recipients = topology.recipients(input.sessionId, tx);
|
|
177
|
+
const messages = recipients.map(recipient => tx.enqueue(recipient, messageForEvent(change, id)));
|
|
178
|
+
let runtime = null;
|
|
179
|
+
if (input.runtime) {
|
|
180
|
+
runtime = runtimeBindings.bind(tx, input.sessionId, input.runtime);
|
|
181
|
+
}
|
|
182
|
+
return {
|
|
183
|
+
state: { sessionId: input.sessionId, status, proposal, note, parentSessionId, updatedAtMs },
|
|
184
|
+
event,
|
|
185
|
+
edge,
|
|
186
|
+
recipients,
|
|
187
|
+
messages,
|
|
188
|
+
runtime,
|
|
189
|
+
};
|
|
190
|
+
});
|
|
191
|
+
const result = {
|
|
192
|
+
state: created.state,
|
|
193
|
+
event: created.event,
|
|
194
|
+
edge: created.edge,
|
|
195
|
+
recipients: created.recipients,
|
|
196
|
+
messages: created.messages,
|
|
197
|
+
};
|
|
198
|
+
notifyCommitted(result);
|
|
199
|
+
return created.state;
|
|
200
|
+
},
|
|
201
|
+
transitionSession(sessionId, input) {
|
|
202
|
+
requireId(sessionId, 'sessionId');
|
|
203
|
+
const nextStatus = input.status;
|
|
204
|
+
if (nextStatus !== undefined)
|
|
205
|
+
requireStatus(nextStatus);
|
|
206
|
+
if (input.parentSessionId !== undefined && input.parentSessionId !== null)
|
|
207
|
+
requireId(input.parentSessionId, 'parentSessionId');
|
|
208
|
+
if (input.parentSessionId === sessionId)
|
|
209
|
+
throw new TypeError('a session cannot be its own parent');
|
|
210
|
+
if (input.parentSessionId)
|
|
211
|
+
initialize(input.parentSessionId);
|
|
212
|
+
const result = protocol.withTransaction(tx => {
|
|
213
|
+
const current = readStateInTransaction(tx, sessionId);
|
|
214
|
+
if (!current) {
|
|
215
|
+
throw new Error(`session application state is missing: ${sessionId}`);
|
|
216
|
+
}
|
|
217
|
+
const status = nextStatus ?? current.status;
|
|
218
|
+
const proposal = input.proposal === undefined ? current.proposal : input.proposal;
|
|
219
|
+
const note = input.note === undefined ? current.note : input.note;
|
|
220
|
+
const parentSessionId = input.parentSessionId === undefined ? current.parentSessionId : input.parentSessionId;
|
|
221
|
+
let edge = null;
|
|
222
|
+
if (parentSessionId !== current.parentSessionId) {
|
|
223
|
+
if (parentSessionId && current.parentSessionId)
|
|
224
|
+
edge = topology.reparent(tx, sessionId, parentSessionId, PARENT_RELATION);
|
|
225
|
+
else if (parentSessionId)
|
|
226
|
+
edge = topology.attach(tx, parentSessionId, sessionId, PARENT_RELATION);
|
|
227
|
+
else if (current.parentSessionId) {
|
|
228
|
+
const former = topology.parents(sessionId, PARENT_RELATION, tx)[0];
|
|
229
|
+
if (former)
|
|
230
|
+
topology.detach(tx, former.edgeId);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
const updatedAtMs = now();
|
|
234
|
+
tx.exec('UPDATE session_application_state SET status=?, proposal=?, note=?, parent_session_id=?, updated_at_ms=? WHERE session_id=?', status, proposal, note, parentSessionId, updatedAtMs, sessionId);
|
|
235
|
+
const id = eventId();
|
|
236
|
+
const change = {
|
|
237
|
+
sessionId,
|
|
238
|
+
status,
|
|
239
|
+
proposal,
|
|
240
|
+
note,
|
|
241
|
+
previousProposal: current.proposal,
|
|
242
|
+
previousNote: current.note,
|
|
243
|
+
parentSessionId,
|
|
244
|
+
previousStatus: current.status,
|
|
245
|
+
previousParentSessionId: current.parentSessionId,
|
|
246
|
+
reason: input.reason ?? null,
|
|
247
|
+
};
|
|
248
|
+
const event = events.append(tx, {
|
|
249
|
+
eventId: id,
|
|
250
|
+
type: STATE_EVENT,
|
|
251
|
+
schemaVersion: 1,
|
|
252
|
+
subjectSessionId: sessionId,
|
|
253
|
+
payload: encodeEventJson(change),
|
|
254
|
+
occurredAtMs: updatedAtMs,
|
|
255
|
+
});
|
|
256
|
+
const recipients = input.recipientSessionIds === undefined
|
|
257
|
+
? lifecycleRecipients(sessionId, current.status, status, tx)
|
|
258
|
+
: [...new Set(input.recipientSessionIds.map(recipient => {
|
|
259
|
+
requireId(recipient, 'recipientSessionId');
|
|
260
|
+
return recipient;
|
|
261
|
+
}))];
|
|
262
|
+
const messages = recipients.map(recipient => tx.enqueue(recipient, messageForEvent(change, id)));
|
|
263
|
+
return {
|
|
264
|
+
state: { sessionId, status, proposal, note, parentSessionId, updatedAtMs },
|
|
265
|
+
event,
|
|
266
|
+
edge,
|
|
267
|
+
recipients,
|
|
268
|
+
messages,
|
|
269
|
+
};
|
|
270
|
+
});
|
|
271
|
+
notifyCommitted(result);
|
|
272
|
+
return result;
|
|
273
|
+
},
|
|
274
|
+
enqueueMessage(sessionId, message) {
|
|
275
|
+
requireId(sessionId, 'sessionId');
|
|
276
|
+
initialize(sessionId);
|
|
277
|
+
return protocol.withTransaction(tx => tx.enqueue(sessionId, message));
|
|
278
|
+
},
|
|
279
|
+
enqueueConversationMessage(sessionId, message, conversation) {
|
|
280
|
+
requireId(sessionId, 'sessionId');
|
|
281
|
+
if (!conversation || typeof conversation.text !== 'string' || conversation.text.length === 0) {
|
|
282
|
+
throw new TypeError('conversation message requires non-empty text');
|
|
283
|
+
}
|
|
284
|
+
if (conversation.from !== null && typeof conversation.from !== 'string') {
|
|
285
|
+
throw new TypeError('conversation message from must be a session id or null');
|
|
286
|
+
}
|
|
287
|
+
initialize(sessionId);
|
|
288
|
+
return protocol.withTransaction(tx => {
|
|
289
|
+
const queued = tx.enqueue(sessionId, message);
|
|
290
|
+
const recorded = events.hasMessageEvent(tx, sessionId, queued.messageId);
|
|
291
|
+
if (!recorded) {
|
|
292
|
+
events.append(tx, {
|
|
293
|
+
eventId: eventId(),
|
|
294
|
+
type: MESSAGE_EVENT,
|
|
295
|
+
schemaVersion: 1,
|
|
296
|
+
subjectSessionId: sessionId,
|
|
297
|
+
payload: encodeEventJson({
|
|
298
|
+
messageId: queued.messageId,
|
|
299
|
+
text: conversation.text,
|
|
300
|
+
from: conversation.from,
|
|
301
|
+
...(conversation.replyVia ? { replyVia: conversation.replyVia } : {}),
|
|
302
|
+
}),
|
|
303
|
+
occurredAtMs: queued.enqueuedAtMs,
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
return queued;
|
|
307
|
+
});
|
|
308
|
+
},
|
|
309
|
+
attachWatcher(watcherSessionId, subjectSessionId, channel = WATCH_RELATION) {
|
|
310
|
+
requireId(watcherSessionId, 'watcherSessionId');
|
|
311
|
+
requireId(subjectSessionId, 'subjectSessionId');
|
|
312
|
+
initialize(watcherSessionId);
|
|
313
|
+
initialize(subjectSessionId);
|
|
314
|
+
return protocol.withTransaction(tx => topology.subscribe(tx, watcherSessionId, subjectSessionId, channel));
|
|
315
|
+
},
|
|
316
|
+
detachWatcher(watcherSessionId, subjectSessionId, channel = WATCH_RELATION) {
|
|
317
|
+
requireId(watcherSessionId, 'watcherSessionId');
|
|
318
|
+
requireId(subjectSessionId, 'subjectSessionId');
|
|
319
|
+
return protocol.withTransaction(tx => topology.unsubscribe(tx, watcherSessionId, subjectSessionId, channel));
|
|
320
|
+
},
|
|
321
|
+
listWatchers(watcherSessionId, channel = WATCH_RELATION) {
|
|
322
|
+
requireId(watcherSessionId, 'watcherSessionId');
|
|
323
|
+
return topology.subscriptions(watcherSessionId).filter(edge => edge.relationType === channel);
|
|
324
|
+
},
|
|
325
|
+
bindRuntime(sessionId, identity, expectedGeneration) {
|
|
326
|
+
requireId(sessionId, 'sessionId');
|
|
327
|
+
initialize(sessionId);
|
|
328
|
+
const binding = protocol.withTransaction(tx => runtimeBindings.bind(tx, sessionId, identity, { expectedGeneration }));
|
|
329
|
+
options.onRuntimeBound?.(sessionId);
|
|
330
|
+
return binding;
|
|
331
|
+
},
|
|
332
|
+
resolveRuntime(sessionId, namespace) {
|
|
333
|
+
requireId(sessionId, 'sessionId');
|
|
334
|
+
return runtimeBindings.resolve(namespace, sessionId);
|
|
335
|
+
},
|
|
336
|
+
dequeueForRuntime(sessionId, namespace, expectedGeneration, expectedMessageId) {
|
|
337
|
+
requireId(sessionId, 'sessionId');
|
|
338
|
+
const binding = runtimeBindings.resolve(namespace, sessionId);
|
|
339
|
+
if (!binding || binding.status !== 'bound')
|
|
340
|
+
throw new Error(`runtime binding is not active for ${sessionId}`);
|
|
341
|
+
if (expectedGeneration !== undefined && binding.bindingGeneration !== expectedGeneration) {
|
|
342
|
+
throw new Error(`runtime binding generation is stale for ${sessionId}`);
|
|
343
|
+
}
|
|
344
|
+
if (expectedMessageId !== undefined && protocol.listPending(sessionId)[0]?.messageId !== expectedMessageId)
|
|
345
|
+
return null;
|
|
346
|
+
return protocol.dequeue(sessionId);
|
|
347
|
+
},
|
|
348
|
+
readPendingMessages(sessionId) {
|
|
349
|
+
requireId(sessionId, 'sessionId');
|
|
350
|
+
return protocol.listPending(sessionId);
|
|
351
|
+
},
|
|
352
|
+
readMessageHistory(sessionId) {
|
|
353
|
+
requireId(sessionId, 'sessionId');
|
|
354
|
+
return protocol.readMessages(sessionId);
|
|
355
|
+
},
|
|
356
|
+
dequeuePendingMessage(sessionId, expectedMessageId) {
|
|
357
|
+
requireId(sessionId, 'sessionId');
|
|
358
|
+
requireId(expectedMessageId, 'expectedMessageId');
|
|
359
|
+
const head = protocol.listPending(sessionId)[0];
|
|
360
|
+
if (!head || head.messageId !== expectedMessageId)
|
|
361
|
+
return null;
|
|
362
|
+
return protocol.dequeue(sessionId);
|
|
363
|
+
},
|
|
364
|
+
readState(sessionId) {
|
|
365
|
+
requireId(sessionId, 'sessionId');
|
|
366
|
+
const state = protocol.withTransaction(tx => readStateInTransaction(tx, sessionId));
|
|
367
|
+
return state;
|
|
368
|
+
},
|
|
369
|
+
readEvents(sessionId, afterSequence) {
|
|
370
|
+
requireId(sessionId, 'sessionId');
|
|
371
|
+
return events.read(sessionId, afterSequence === undefined ? undefined : { afterSequence });
|
|
372
|
+
},
|
|
373
|
+
readFollowCursor(watcherSessionId, subjectSessionId) {
|
|
374
|
+
requireId(watcherSessionId, 'watcherSessionId');
|
|
375
|
+
requireId(subjectSessionId, 'subjectSessionId');
|
|
376
|
+
const rows = protocol.withTransaction(tx => tx.query('SELECT event_seq FROM session_follow_cursors WHERE watcher_session_id=? AND subject_session_id=?', watcherSessionId, subjectSessionId));
|
|
377
|
+
const value = rows[0]?.event_seq;
|
|
378
|
+
return value === undefined ? null : Number(value);
|
|
379
|
+
},
|
|
380
|
+
advanceFollowCursor(watcherSessionId, subjectSessionId, eventSequence) {
|
|
381
|
+
requireId(watcherSessionId, 'watcherSessionId');
|
|
382
|
+
requireId(subjectSessionId, 'subjectSessionId');
|
|
383
|
+
if (!Number.isSafeInteger(eventSequence) || eventSequence < 0)
|
|
384
|
+
throw new TypeError('eventSequence must be a non-negative safe integer');
|
|
385
|
+
protocol.withTransaction(tx => {
|
|
386
|
+
const prior = tx.query('SELECT event_seq FROM session_follow_cursors WHERE watcher_session_id=? AND subject_session_id=?', watcherSessionId, subjectSessionId)[0]?.event_seq;
|
|
387
|
+
if (prior !== undefined && Number(prior) >= eventSequence)
|
|
388
|
+
return;
|
|
389
|
+
tx.exec(`INSERT INTO session_follow_cursors (watcher_session_id, subject_session_id, event_seq)
|
|
390
|
+
VALUES (?, ?, ?)
|
|
391
|
+
ON CONFLICT(watcher_session_id, subject_session_id) DO UPDATE SET event_seq=excluded.event_seq`, watcherSessionId, subjectSessionId, eventSequence);
|
|
392
|
+
});
|
|
393
|
+
},
|
|
394
|
+
replayState(sessionId) {
|
|
395
|
+
requireId(sessionId, 'sessionId');
|
|
396
|
+
initialize(sessionId);
|
|
397
|
+
return events.replay(sessionId, {
|
|
398
|
+
initialState: null,
|
|
399
|
+
reducers: {
|
|
400
|
+
// A conversation message is a public fact beside the state fold, not a state transition.
|
|
401
|
+
[MESSAGE_EVENT]: state => state,
|
|
402
|
+
[STATE_EVENT]: (_state, event) => {
|
|
403
|
+
const decoded = JSON.parse(new TextDecoder().decode(event.payload));
|
|
404
|
+
return {
|
|
405
|
+
sessionId: decoded.sessionId,
|
|
406
|
+
status: decoded.status,
|
|
407
|
+
proposal: decoded.proposal ?? null,
|
|
408
|
+
note: decoded.note ?? null,
|
|
409
|
+
parentSessionId: decoded.parentSessionId,
|
|
410
|
+
updatedAtMs: event.occurredAtMs,
|
|
411
|
+
};
|
|
412
|
+
},
|
|
413
|
+
},
|
|
414
|
+
});
|
|
415
|
+
},
|
|
416
|
+
close() {
|
|
417
|
+
compositions.delete(options.databasePath);
|
|
418
|
+
closeProtocol(options.databasePath, protocol);
|
|
419
|
+
},
|
|
420
|
+
};
|
|
421
|
+
compositions.set(options.databasePath, app);
|
|
422
|
+
return app;
|
|
423
|
+
}
|
|
424
|
+
const protocols = new Map();
|
|
425
|
+
function openProtocolOnce(databasePath, now) {
|
|
426
|
+
const existing = protocols.get(databasePath);
|
|
427
|
+
if (existing)
|
|
428
|
+
return existing;
|
|
429
|
+
const protocol = openProtocol(databasePath, now ? { now } : undefined);
|
|
430
|
+
protocols.set(databasePath, protocol);
|
|
431
|
+
return protocol;
|
|
432
|
+
}
|
|
433
|
+
function closeProtocol(databasePath, protocol) {
|
|
434
|
+
if (protocols.get(databasePath) !== protocol)
|
|
435
|
+
return;
|
|
436
|
+
protocols.delete(databasePath);
|
|
437
|
+
protocol.close();
|
|
438
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export const SESSION_APPLICATION_MIGRATIONS = [
|
|
2
|
+
{
|
|
3
|
+
version: 1,
|
|
4
|
+
sql: `
|
|
5
|
+
CREATE TABLE session_application_state (
|
|
6
|
+
session_id TEXT NOT NULL PRIMARY KEY REFERENCES protocol_sessions(session_id),
|
|
7
|
+
status TEXT NOT NULL,
|
|
8
|
+
parent_session_id TEXT REFERENCES protocol_sessions(session_id),
|
|
9
|
+
updated_at_ms INTEGER NOT NULL,
|
|
10
|
+
CHECK (length(status) BETWEEN 1 AND 64),
|
|
11
|
+
CHECK (status NOT GLOB '*[^0-9A-Za-z._:-]*'),
|
|
12
|
+
CHECK (updated_at_ms >= 0),
|
|
13
|
+
CHECK (parent_session_id IS NULL OR parent_session_id <> session_id)
|
|
14
|
+
) STRICT;
|
|
15
|
+
|
|
16
|
+
CREATE INDEX session_application_state_parent
|
|
17
|
+
ON session_application_state (parent_session_id, session_id);
|
|
18
|
+
`,
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
version: 2,
|
|
22
|
+
sql: `
|
|
23
|
+
ALTER TABLE session_application_state ADD COLUMN proposal TEXT;
|
|
24
|
+
ALTER TABLE session_application_state ADD COLUMN note TEXT;
|
|
25
|
+
`,
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
version: 3,
|
|
29
|
+
sql: `
|
|
30
|
+
CREATE TABLE session_follow_cursors (
|
|
31
|
+
watcher_session_id TEXT NOT NULL REFERENCES protocol_sessions(session_id),
|
|
32
|
+
subject_session_id TEXT NOT NULL REFERENCES protocol_sessions(session_id),
|
|
33
|
+
event_seq INTEGER NOT NULL,
|
|
34
|
+
PRIMARY KEY (watcher_session_id, subject_session_id),
|
|
35
|
+
CHECK (event_seq >= 0)
|
|
36
|
+
) STRICT;
|
|
37
|
+
`,
|
|
38
|
+
},
|
|
39
|
+
];
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@spexcode/session-application",
|
|
3
|
+
"version": "0.6.8",
|
|
4
|
+
"publishConfig": { "access": "public" },
|
|
5
|
+
"type": "module",
|
|
6
|
+
"description": "Adopter-owned composition of session protocol and topology transactions.",
|
|
7
|
+
"files": ["dist"],
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./dist/index.js",
|
|
10
|
+
"./package.json": "./package.json"
|
|
11
|
+
},
|
|
12
|
+
"engines": { "node": ">=22" },
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "node ../../scripts/build-dist.mjs",
|
|
15
|
+
"prepublishOnly": "node ../../scripts/release-publish.mjs --from-package-publish",
|
|
16
|
+
"test": "npm run build && tsx --import ../../scripts/test-home.mjs --test src/*.test.ts"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@spexcode/session-events": "0.6.8",
|
|
20
|
+
"@spexcode/session-protocol": "0.6.8",
|
|
21
|
+
"@spexcode/session-runtime": "0.6.8",
|
|
22
|
+
"@spexcode/session-topology": "0.6.8"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@types/node": "^20.16.0",
|
|
26
|
+
"tsx": "^4.19.2",
|
|
27
|
+
"typescript": "^5.6.3"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export type SessionEventErrorCode = 'EVENT_TRANSACTION_INVALID' | 'EVENT_SESSION_ID_INVALID' | 'EVENT_SESSION_UNKNOWN' | 'EVENT_ID_INVALID' | 'EVENT_ID_EXISTS' | 'EVENT_TYPE_INVALID' | 'EVENT_SCHEMA_VERSION_INVALID' | 'EVENT_PAYLOAD_INVALID' | 'EVENT_TIMESTAMP_INVALID' | 'EVENT_SEQUENCE_INVALID' | 'EVENT_TYPE_UNKNOWN' | 'EVENT_JSON_INVALID' | 'EVENT_STORAGE';
|
|
2
|
+
export declare class SessionEventError extends Error {
|
|
3
|
+
readonly code: SessionEventErrorCode;
|
|
4
|
+
constructor(code: SessionEventErrorCode, message: string, cause?: unknown);
|
|
5
|
+
}
|
|
6
|
+
export declare function failSessionEvent(code: SessionEventErrorCode, message: string, cause?: unknown): never;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export class SessionEventError extends Error {
|
|
2
|
+
code;
|
|
3
|
+
constructor(code, message, cause) {
|
|
4
|
+
super(message, { cause });
|
|
5
|
+
this.name = 'SessionEventError';
|
|
6
|
+
this.code = code;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export function failSessionEvent(code, message, cause) {
|
|
10
|
+
throw new SessionEventError(code, message, cause);
|
|
11
|
+
}
|