@astrosheep/square 0.3.61 → 0.3.62
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/claude-plugin/.claude-plugin/plugin.json +1 -1
- package/claude-plugin/skills/square/SKILL.md +5 -7
- package/codex-plugin/.codex-plugin/plugin.json +1 -1
- package/dist/activity.d.ts +6 -2
- package/dist/activity.js +168 -133
- package/dist/artifact.d.ts +4 -0
- package/dist/artifact.js +43 -14
- package/dist/attention-presentation.d.ts +1 -0
- package/dist/attention-presentation.js +8 -6
- package/dist/automatic-session.js +14 -24
- package/dist/boundary-presentation.d.ts +1 -2
- package/dist/boundary-presentation.js +4 -19
- package/dist/cli/context.d.ts +2 -0
- package/dist/cli/context.js +2 -18
- package/dist/cli/harness-command.js +1 -1
- package/dist/cli/mcp-server-command.d.ts +2 -0
- package/dist/cli/mcp-server-command.js +13 -0
- package/dist/cli/observation-commands.js +47 -19
- package/dist/cli/program.js +5 -2
- package/dist/cli/registry.js +2 -0
- package/dist/cli/square-commands.d.ts +1 -1
- package/dist/cli/square-commands.js +93 -125
- package/dist/decisions.d.ts +2 -0
- package/dist/decisions.js +44 -3
- package/dist/delivery-operations.d.ts +1 -2
- package/dist/delivery-operations.js +1 -4
- package/dist/file-changes.d.ts +12 -0
- package/dist/file-changes.js +235 -0
- package/dist/help.js +4 -8
- package/dist/host-ledger-file-adapter.d.ts +15 -13
- package/dist/host-ledger-file-adapter.js +131 -79
- package/dist/host-ledger-root.d.ts +2 -0
- package/dist/host-ledger-root.js +9 -0
- package/dist/host-ledger.d.ts +4 -24
- package/dist/inbox.d.ts +10 -5
- package/dist/inbox.js +152 -67
- package/dist/index.d.ts +3 -1
- package/dist/index.js +1 -0
- package/dist/mcp.d.ts +7 -0
- package/dist/mcp.js +122 -0
- package/dist/mention-parse.d.ts +8 -0
- package/dist/mention-parse.js +107 -0
- package/dist/model.d.ts +5 -0
- package/dist/model.js +4 -1
- package/dist/notifications.d.ts +1 -0
- package/dist/notifications.js +25 -20
- package/dist/participant-host.js +2 -2
- package/dist/ports.d.ts +2 -9
- package/dist/presentation.d.ts +12 -1
- package/dist/presentation.js +42 -12
- package/dist/presented.js +2 -1
- package/dist/registry.d.ts +8 -4
- package/dist/registry.js +41 -74
- package/dist/routes.d.ts +19 -2
- package/dist/routes.js +48 -5
- package/dist/runtime.d.ts +2 -0
- package/dist/runtime.js +2 -0
- package/dist/square-actions.d.ts +26 -13
- package/dist/square-actions.js +200 -84
- package/dist/square-application.d.ts +58 -0
- package/dist/square-application.js +183 -0
- package/dist/square-facade.d.ts +24 -9
- package/dist/square-file-adapter.d.ts +5 -12
- package/dist/square-file-adapter.js +12 -64
- package/dist/square-projections.d.ts +3 -2
- package/dist/square-projections.js +12 -3
- package/dist/square-storage.js +27 -34
- package/dist/square-wiring.d.ts +12 -7
- package/dist/square-wiring.js +36 -32
- package/dist/state-cell.d.ts +1 -1
- package/dist/stream.js +30 -26
- package/dist/views.d.ts +3 -3
- package/dist/views.js +4 -4
- package/dist/wake-attempts.js +2 -1
- package/dist/wake-evidence.js +2 -8
- package/dist/wakes.d.ts +1 -0
- package/dist/wakes.js +5 -5
- package/dist/watch.d.ts +6 -1
- package/dist/watch.js +20 -18
- package/extensions/square-pi.js +100 -112
- package/guides/application-boundary.md +32 -0
- package/guides/change-waits.md +28 -0
- package/guides/host-ledger.md +9 -0
- package/guides/mcp.md +9 -0
- package/guides/operation-cancellation.md +17 -0
- package/package.json +8 -2
- package/skills/square/SKILL.md +5 -7
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { setTimeout as sleep } from 'node:timers/promises';
|
|
3
|
+
import { SquareError } from './model.js';
|
|
4
|
+
import { validateDoneBody } from './decisions.js';
|
|
5
|
+
import { currentHold, HELD_WAIT_BUDGET_MS } from './runtime.js';
|
|
6
|
+
import { Square, openParticipant } from './square-wiring.js';
|
|
7
|
+
import { openSquare } from './square-file-adapter.js';
|
|
8
|
+
import { closeOpenSquare } from './open-square.js';
|
|
9
|
+
import { statusPresentation, participantsPresentation } from './views.js';
|
|
10
|
+
import { hostLedgerForEnv, localParticipantName, localSessionIdentities, readParticipantOwner } from './registry.js';
|
|
11
|
+
function requireLocation(context) {
|
|
12
|
+
const selected = context.squarePath ?? context.env.SQUARE_LOCATION;
|
|
13
|
+
if (selected === undefined || selected.trim() === '')
|
|
14
|
+
throw new SquareError('invalid_args', 'Square application context needs a square location');
|
|
15
|
+
return path.resolve(context.cwd, selected);
|
|
16
|
+
}
|
|
17
|
+
async function requireParticipant(context, squarePath, env = context.env) {
|
|
18
|
+
const selected = context.participant ?? env.SQUARE_PARTICIPANT_NAME;
|
|
19
|
+
if (selected?.trim())
|
|
20
|
+
return selected;
|
|
21
|
+
const discovered = await localParticipantName(squarePath, env).catch(() => undefined);
|
|
22
|
+
if (discovered !== undefined)
|
|
23
|
+
return discovered;
|
|
24
|
+
throw new SquareError('invalid_args', 'Square application context needs one unambiguous participant name');
|
|
25
|
+
}
|
|
26
|
+
function checkControl(control) {
|
|
27
|
+
if (control?.signal?.aborted)
|
|
28
|
+
throw new SquareError('invalid_args', 'Square operation was cancelled');
|
|
29
|
+
}
|
|
30
|
+
export function createSquareApplication(context) {
|
|
31
|
+
const env = { ...context.env };
|
|
32
|
+
const location = () => requireLocation({ ...context, env });
|
|
33
|
+
const hostLedger = context.hostLedger ?? hostLedgerForEnv(env);
|
|
34
|
+
const open = () => Square.at({ path: location(), clock: context.clock, env, hostLedger, wakeTransport: context.wakeTransport });
|
|
35
|
+
async function existing(operation, control) {
|
|
36
|
+
checkControl(control);
|
|
37
|
+
const squarePath = location();
|
|
38
|
+
const participantName = await requireParticipant(context, squarePath, env);
|
|
39
|
+
const facade = await openParticipant({ path: squarePath, clock: context.clock, env, hostLedger, wakeTransport: context.wakeTransport }, participantName);
|
|
40
|
+
try {
|
|
41
|
+
checkControl(control);
|
|
42
|
+
return await operation(facade.participant);
|
|
43
|
+
}
|
|
44
|
+
finally {
|
|
45
|
+
await facade.close();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
async function joined(operation, control) {
|
|
49
|
+
checkControl(control);
|
|
50
|
+
const square = await open();
|
|
51
|
+
try {
|
|
52
|
+
return await operation(await square.join(await requireParticipant(context, square.location, env), control));
|
|
53
|
+
}
|
|
54
|
+
finally {
|
|
55
|
+
await square.close();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
async join(options = {}, control = {}) {
|
|
60
|
+
checkControl(control);
|
|
61
|
+
const squarePath = location();
|
|
62
|
+
const square = await open();
|
|
63
|
+
try {
|
|
64
|
+
const participantName = await requireParticipant(context, squarePath, env);
|
|
65
|
+
const before = await square.snapshot();
|
|
66
|
+
const standing = before.participants.some((item) => item.name.toLocaleLowerCase() === participantName.toLocaleLowerCase() && item.state === 'joined');
|
|
67
|
+
const identity = localSessionIdentities(env)[0];
|
|
68
|
+
const owner = standing ? await readParticipantOwner(squarePath, participantName, env).catch(() => undefined) : undefined;
|
|
69
|
+
const reconnect = standing && identity !== undefined && owner?.sessionId === identity.sessionId;
|
|
70
|
+
if (standing && owner !== undefined && identity !== undefined && !options.takeover && !reconnect) {
|
|
71
|
+
throw new SquareError('already_joined', `✕ ${participantName} already stands here — another session holds the name`, { pending: before.participants.length });
|
|
72
|
+
}
|
|
73
|
+
if (options.takeover && standing) {
|
|
74
|
+
const replaced = await square.takeoverWithActivity(participantName, control);
|
|
75
|
+
const activity = replaced.activities.at(-1) ?? null;
|
|
76
|
+
return { participant: replaced.participant.name, activity, kind: 'taken-over', participantCount: (await square.snapshot()).participants.length };
|
|
77
|
+
}
|
|
78
|
+
const joined = await square.joinWithActivity(participantName, control);
|
|
79
|
+
return { participant: joined.participant.name, activity: joined.activity, kind: reconnect ? 'reconnected' : 'joined', participantCount: (await square.snapshot()).participants.length };
|
|
80
|
+
}
|
|
81
|
+
finally {
|
|
82
|
+
await square.close();
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
async express(body, options = {}, control = {}) {
|
|
86
|
+
let waited = false;
|
|
87
|
+
let heldWaitedMs = 0;
|
|
88
|
+
while (true) {
|
|
89
|
+
checkControl(control);
|
|
90
|
+
try {
|
|
91
|
+
const result = await existing((participant) => participant.express(body, options, control), control);
|
|
92
|
+
return { ...result, waited };
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
if (!(error instanceof SquareError) || options.noWait || (error.code !== 'held' && error.code !== 'throttled'))
|
|
96
|
+
throw error;
|
|
97
|
+
// A held wait is a convenience, not a trap: past the budget the error
|
|
98
|
+
// surfaces exactly like --no-wait (the CLI saves a draft and prints a retry).
|
|
99
|
+
if (error.code === 'held' && heldWaitedMs >= HELD_WAIT_BUDGET_MS)
|
|
100
|
+
throw error;
|
|
101
|
+
waited = true;
|
|
102
|
+
control?.onProgress?.({
|
|
103
|
+
kind: 'waiting',
|
|
104
|
+
reason: error.code,
|
|
105
|
+
...(error.code === 'throttled' ? { delayMs: error.facts?.retryAfterMs ?? 1 } : {}),
|
|
106
|
+
...(error.facts?.holder === undefined ? {} : { holder: error.facts.holder }),
|
|
107
|
+
...(error.facts?.holdReason === undefined ? {} : { holdReason: error.facts.holdReason }),
|
|
108
|
+
});
|
|
109
|
+
if (error.code === 'throttled') {
|
|
110
|
+
await sleep(error.facts?.retryAfterMs ?? 1, undefined, { signal: control.signal });
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
const waitStarted = Date.now();
|
|
114
|
+
const square = await openSquare(location(), { clock: context.clock, env, hostLedger, signal: control.signal });
|
|
115
|
+
try {
|
|
116
|
+
const snapshot = await square.artifact.read(control.signal);
|
|
117
|
+
if (currentHold(snapshot.state.acts).active)
|
|
118
|
+
await square.artifact.changed(snapshot.version, Math.max(1, HELD_WAIT_BUDGET_MS - heldWaitedMs), control.signal);
|
|
119
|
+
}
|
|
120
|
+
finally {
|
|
121
|
+
await closeOpenSquare(square);
|
|
122
|
+
}
|
|
123
|
+
heldWaitedMs += Date.now() - waitStarted;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
catch(options, control) { return existing((participant) => participant.catch(options, control), control); },
|
|
129
|
+
async history(query, control) {
|
|
130
|
+
checkControl(control);
|
|
131
|
+
if (context.participant !== undefined || env.SQUARE_PARTICIPANT_NAME !== undefined)
|
|
132
|
+
return existing((participant) => participant.history(query, control), control);
|
|
133
|
+
const square = await open();
|
|
134
|
+
try {
|
|
135
|
+
checkControl(control);
|
|
136
|
+
return await square.history(query, control);
|
|
137
|
+
}
|
|
138
|
+
finally {
|
|
139
|
+
await square.close();
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
listen(target, control) { return existing((participant) => participant.listen(target, control), control); },
|
|
143
|
+
ignore(target, control) { return existing((participant) => participant.ignore(target, control), control); },
|
|
144
|
+
listening(control) { return existing((participant) => participant.listening(control), control); },
|
|
145
|
+
hold(reason, control) { return joined((participant) => participant.hold(reason, control), control); },
|
|
146
|
+
resume(control) { return joined((participant) => participant.resume(control), control); },
|
|
147
|
+
async done(body, control) {
|
|
148
|
+
validateDoneBody(body);
|
|
149
|
+
const squarePath = location();
|
|
150
|
+
const participantName = await requireParticipant(context, squarePath, env);
|
|
151
|
+
const identity = localSessionIdentities(env)[0];
|
|
152
|
+
const owner = identity === undefined ? undefined : await readParticipantOwner(squarePath, participantName, env).catch(() => undefined);
|
|
153
|
+
if (identity !== undefined && owner?.sessionId === identity.sessionId && owner.epoch > 0) {
|
|
154
|
+
const square = await open();
|
|
155
|
+
try {
|
|
156
|
+
return await square.doneOwnedSession(participantName, body ?? '', owner.epoch, control);
|
|
157
|
+
}
|
|
158
|
+
finally {
|
|
159
|
+
await square.close();
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return joined((participant) => participant.done(body, control), control);
|
|
163
|
+
},
|
|
164
|
+
async status() {
|
|
165
|
+
const square = await openSquare(location(), { clock: context.clock, env, hostLedger, wakeTransport: context.wakeTransport });
|
|
166
|
+
try {
|
|
167
|
+
return await statusPresentation(square);
|
|
168
|
+
}
|
|
169
|
+
finally {
|
|
170
|
+
await closeOpenSquare(square);
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
async participants() {
|
|
174
|
+
const square = await openSquare(location(), { clock: context.clock, env, hostLedger, wakeTransport: context.wakeTransport });
|
|
175
|
+
try {
|
|
176
|
+
return await participantsPresentation(square);
|
|
177
|
+
}
|
|
178
|
+
finally {
|
|
179
|
+
await closeOpenSquare(square);
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
}
|
package/dist/square-facade.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ export interface Activity {
|
|
|
7
7
|
readonly actor: string;
|
|
8
8
|
readonly body?: string;
|
|
9
9
|
readonly mentions: readonly string[];
|
|
10
|
+
readonly reach?: Reach;
|
|
10
11
|
readonly target?: string;
|
|
11
12
|
readonly reply?: ActivityId;
|
|
12
13
|
}
|
|
@@ -18,6 +19,7 @@ export interface ExpressOptions {
|
|
|
18
19
|
readonly mentions?: readonly string[];
|
|
19
20
|
readonly reach?: Reach;
|
|
20
21
|
readonly reply?: ActivityId;
|
|
22
|
+
readonly noMention?: boolean;
|
|
21
23
|
}
|
|
22
24
|
export interface ExpressResult {
|
|
23
25
|
readonly activity: Activity;
|
|
@@ -39,12 +41,24 @@ export interface CatchResult {
|
|
|
39
41
|
readonly idleExpired: boolean;
|
|
40
42
|
readonly remaining: number;
|
|
41
43
|
}
|
|
44
|
+
export interface OperationProgress {
|
|
45
|
+
readonly kind: 'waiting';
|
|
46
|
+
readonly reason: 'held' | 'throttled';
|
|
47
|
+
readonly delayMs?: number;
|
|
48
|
+
readonly holder?: string;
|
|
49
|
+
readonly holdReason?: string;
|
|
50
|
+
}
|
|
51
|
+
export interface OperationControl {
|
|
52
|
+
readonly signal?: AbortSignal;
|
|
53
|
+
readonly onProgress?: (progress: OperationProgress) => void;
|
|
54
|
+
}
|
|
42
55
|
export interface HistoryQuery {
|
|
43
56
|
readonly limit?: number;
|
|
44
57
|
readonly order?: 'asc' | 'desc';
|
|
45
58
|
readonly before?: ActivityId;
|
|
46
59
|
readonly after?: ActivityId;
|
|
47
60
|
readonly grep?: string;
|
|
61
|
+
readonly fixed?: string;
|
|
48
62
|
readonly from?: readonly string[];
|
|
49
63
|
readonly mention?: string;
|
|
50
64
|
}
|
|
@@ -93,13 +107,14 @@ export interface OwnershipFenceOptions {
|
|
|
93
107
|
}
|
|
94
108
|
export interface Participant {
|
|
95
109
|
readonly name: string;
|
|
96
|
-
express(body: string, options?: ExpressOptions): Promise<ExpressResult>;
|
|
97
|
-
listen(target: string): Promise<ListenerChangeResult>;
|
|
98
|
-
ignore(target: string): Promise<ListenerChangeResult>;
|
|
99
|
-
listening(): Promise<readonly string[]>;
|
|
100
|
-
catch(options?: CatchOptions): Promise<CatchResult>;
|
|
101
|
-
history(query?: HistoryQuery): Promise<Activity[]>;
|
|
102
|
-
hold(reason?: string): Promise<ExpressResult>;
|
|
103
|
-
resume(): Promise<ExpressResult>;
|
|
104
|
-
|
|
110
|
+
express(body: string, options?: ExpressOptions, control?: OperationControl): Promise<ExpressResult>;
|
|
111
|
+
listen(target: string, control?: OperationControl): Promise<ListenerChangeResult>;
|
|
112
|
+
ignore(target: string, control?: OperationControl): Promise<ListenerChangeResult>;
|
|
113
|
+
listening(control?: OperationControl): Promise<readonly string[]>;
|
|
114
|
+
catch(options?: CatchOptions, control?: OperationControl): Promise<CatchResult>;
|
|
115
|
+
history(query?: HistoryQuery, control?: OperationControl): Promise<Activity[]>;
|
|
116
|
+
hold(reason?: string, control?: OperationControl): Promise<ExpressResult>;
|
|
117
|
+
resume(control?: OperationControl): Promise<ExpressResult>;
|
|
118
|
+
/** Leave without a message. Only an omitted or empty body is accepted. */
|
|
119
|
+
done(body?: string, control?: OperationControl): Promise<ExpressResult>;
|
|
105
120
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
export { observeSquareRevision as observeSquareChanges } from './artifact.js';
|
|
1
2
|
import { type BuildOptions, type HardCap } from './model.js';
|
|
2
|
-
import {
|
|
3
|
-
import type { HostLedgerPort } from './ports.js';
|
|
3
|
+
import type { OpenSquare } from './open-square.js';
|
|
4
|
+
import type { HostLedgerPort, SquareArtifactPort } from './ports.js';
|
|
4
5
|
/** File-owned artifact creation for the CLI and path-backed public facade. */
|
|
5
6
|
export declare function createSquare(squarePath: string, options: BuildOptions & {
|
|
6
7
|
hardCap: HardCap;
|
|
@@ -20,13 +21,5 @@ export declare function openSquare(squarePath: string, options?: Pick<SquareBuil
|
|
|
20
21
|
export declare function probeSquare(squarePath: string): Promise<OpenSquare | undefined>;
|
|
21
22
|
export declare function buildSquare(squarePath: string, options: SquareBuildOptions): Promise<OpenSquare>;
|
|
22
23
|
export declare function buildMemorySquare(options: SquareBuildOptions): OpenSquare;
|
|
23
|
-
/**
|
|
24
|
-
export
|
|
25
|
-
status: 'ready';
|
|
26
|
-
value: T;
|
|
27
|
-
} | {
|
|
28
|
-
status: 'changed';
|
|
29
|
-
} | {
|
|
30
|
-
status: 'expired';
|
|
31
|
-
};
|
|
32
|
-
export declare function waitForSquareChanges<T>(squarePaths: readonly string[], timeoutMs: number, signal?: AbortSignal, afterReady?: () => Promise<T | undefined>, onArmed?: (armed: boolean) => void): Promise<SquareChangeWaitResult<T>>;
|
|
24
|
+
/** A projection reads and validates its snapshot once; no preliminary duplicate read. */
|
|
25
|
+
export declare function openSquareArtifact(squarePath: string, signal?: AbortSignal): SquareArtifactPort;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
export { observeSquareRevision as observeSquareChanges } from './artifact.js';
|
|
2
|
+
import { hostLedgerRoot } from './host-ledger-root.js';
|
|
2
3
|
import { createSquareState, createSquareSnapshot, probeSquareFile, writeSquareSnapshot, openSquareCell, createMemoryCell, } from './square-storage.js';
|
|
3
4
|
import { InternalSquareError, SquareError, } from './model.js';
|
|
4
|
-
import { closeOpenSquare } from './open-square.js';
|
|
5
5
|
import { createHostLedgerPort } from './host-ledger-file-adapter.js';
|
|
6
6
|
/** File-owned artifact creation for the CLI and path-backed public facade. */
|
|
7
7
|
export async function createSquare(squarePath, options, snippet) {
|
|
@@ -26,25 +26,23 @@ function validateBuildOptions(options) {
|
|
|
26
26
|
}
|
|
27
27
|
export async function openSquare(squarePath, options = {}) {
|
|
28
28
|
const env = options.env ?? process.env;
|
|
29
|
-
const ledgerRoot =
|
|
30
|
-
const
|
|
29
|
+
const ledgerRoot = hostLedgerRoot(env);
|
|
30
|
+
const artifact = openSquareArtifact(squarePath, options.signal);
|
|
31
31
|
try {
|
|
32
|
-
await
|
|
33
|
-
const artifact = { read: (signal) => cell.read(signal), transact: (fn, signal) => cell.transact(fn, signal), changed: (since, timeout) => cell.changed(since, timeout), close: () => cell.close() };
|
|
32
|
+
await artifact.read();
|
|
34
33
|
return {
|
|
35
34
|
artifact,
|
|
36
35
|
clock: options.clock ?? Date.now,
|
|
37
36
|
location: squarePath,
|
|
38
37
|
env,
|
|
39
38
|
hostLedger: options.hostLedger ?? createHostLedgerPort({
|
|
40
|
-
|
|
41
|
-
localPath: env.SQUARE_HOST_LEDGER_LOCAL ?? ledgerRoot,
|
|
39
|
+
rootPath: ledgerRoot,
|
|
42
40
|
}),
|
|
43
41
|
wakeTransport: options.wakeTransport,
|
|
44
42
|
};
|
|
45
43
|
}
|
|
46
44
|
catch (error) {
|
|
47
|
-
await
|
|
45
|
+
await artifact.close();
|
|
48
46
|
if (error instanceof InternalSquareError && error.code === 'not_found') {
|
|
49
47
|
throw new SquareError('unavailable', `Square is unavailable at ${squarePath}`);
|
|
50
48
|
}
|
|
@@ -81,60 +79,10 @@ export function buildMemorySquare(options) {
|
|
|
81
79
|
}, options.markdown);
|
|
82
80
|
return { artifact: memoryArtifact(createMemoryCell(squareState)), clock: options.clock ?? Date.now, location: 'memory', hostLedger: options.hostLedger, wakeTransport: options.wakeTransport };
|
|
83
81
|
}
|
|
84
|
-
|
|
85
|
-
|
|
82
|
+
/** A projection reads and validates its snapshot once; no preliminary duplicate read. */
|
|
83
|
+
export function openSquareArtifact(squarePath, signal) {
|
|
84
|
+
return memoryArtifact(openSquareCell(squarePath, signal));
|
|
86
85
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
onArmed?.(false);
|
|
90
|
-
return { status: 'expired' };
|
|
91
|
-
}
|
|
92
|
-
const squares = [];
|
|
93
|
-
try {
|
|
94
|
-
for (const squarePath of [...new Set(squarePaths)]) {
|
|
95
|
-
try {
|
|
96
|
-
squares.push(await openSquare(squarePath));
|
|
97
|
-
}
|
|
98
|
-
catch { /* stale binding */ }
|
|
99
|
-
}
|
|
100
|
-
if (squares.length === 0) {
|
|
101
|
-
onArmed?.(false);
|
|
102
|
-
return { status: 'expired' };
|
|
103
|
-
}
|
|
104
|
-
const baselines = await Promise.all(squares.map(async (square) => ({
|
|
105
|
-
square,
|
|
106
|
-
version: (await square.artifact.read()).version,
|
|
107
|
-
})));
|
|
108
|
-
const ready = await afterReady?.();
|
|
109
|
-
if (ready !== undefined) {
|
|
110
|
-
onArmed?.(false);
|
|
111
|
-
return { status: 'ready', value: ready };
|
|
112
|
-
}
|
|
113
|
-
const waits = baselines.map(async ({ square, version }) => {
|
|
114
|
-
const changed = await square.artifact.changed(version, timeoutMs).catch(() => false);
|
|
115
|
-
if (changed)
|
|
116
|
-
return true;
|
|
117
|
-
throw new Error('square wait expired');
|
|
118
|
-
});
|
|
119
|
-
onArmed?.(true);
|
|
120
|
-
let abortWait;
|
|
121
|
-
const abort = new Promise((resolve) => {
|
|
122
|
-
abortWait = () => resolve(false);
|
|
123
|
-
if (signal?.aborted)
|
|
124
|
-
abortWait();
|
|
125
|
-
else
|
|
126
|
-
signal?.addEventListener('abort', abortWait, { once: true });
|
|
127
|
-
});
|
|
128
|
-
try {
|
|
129
|
-
const changed = await Promise.race([Promise.any(waits).catch(() => false), abort]);
|
|
130
|
-
return changed ? { status: 'changed' } : { status: 'expired' };
|
|
131
|
-
}
|
|
132
|
-
finally {
|
|
133
|
-
if (abortWait)
|
|
134
|
-
signal?.removeEventListener('abort', abortWait);
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
finally {
|
|
138
|
-
await Promise.all(squares.map((square) => closeOpenSquare(square)));
|
|
139
|
-
}
|
|
86
|
+
function memoryArtifact(cell) {
|
|
87
|
+
return { read: (signal) => cell.read(signal), transact: (fn, signal) => cell.transact(fn, signal), changed: (since, timeout, signal) => cell.changed(since, timeout, signal), close: () => cell.close() };
|
|
140
88
|
}
|
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import { type SquareState } from './model.js';
|
|
1
|
+
import { type InboxMembership, type SquareState } from './model.js';
|
|
2
2
|
import type { HostLedgerPort, SquareArtifactPort, PresentationEvidenceProjection, PresentationProjection, SessionBindingProjection } from './ports.js';
|
|
3
3
|
import { deriveDeliveryModel } from './delivery.js';
|
|
4
4
|
import type { WakeRoute, WakeRouteKind } from './model.js';
|
|
5
|
+
/** A fresh blocking catch owns only the notifications admitted by its filter. */
|
|
6
|
+
export declare function pendingAtBoundary(inbox: InboxMembership[]): InboxMembership[];
|
|
5
7
|
export declare function projectSessionBindings(input: {
|
|
6
8
|
readonly hostLedger: HostLedgerPort;
|
|
7
9
|
readonly sessionId: string;
|
|
8
10
|
readonly location?: string;
|
|
9
|
-
readonly scopes?: readonly import('./host-ledger.js').HostLedgerScope[];
|
|
10
11
|
readonly now?: number;
|
|
11
12
|
}): Promise<readonly SessionBindingProjection[]>;
|
|
12
13
|
export declare function sessionIdsFromEnvironment(env?: NodeJS.ProcessEnv): readonly string[];
|
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
import { formatActivityId, parseActivityId } from './square-core.js';
|
|
2
2
|
import { nameKey } from './model.js';
|
|
3
|
-
import { deriveDeliveryModel } from './delivery.js';
|
|
3
|
+
import { deriveDeliveryModel, leaseOwnsNotification } from './delivery.js';
|
|
4
4
|
import { freshWatchLease } from './runtime.js';
|
|
5
5
|
import { canonicalRouteLocation } from './routes.js';
|
|
6
|
+
/** A fresh blocking catch owns only the notifications admitted by its filter. */
|
|
7
|
+
export function pendingAtBoundary(inbox) {
|
|
8
|
+
return inbox.map((membership) => {
|
|
9
|
+
const lease = membership.catchLease;
|
|
10
|
+
if (lease === undefined)
|
|
11
|
+
return membership;
|
|
12
|
+
return { ...membership, notifications: membership.notifications.filter((notification) => !leaseOwnsNotification(lease, { ...notification, recipient: membership.name })) };
|
|
13
|
+
}).filter((membership) => membership.notifications.length > 0);
|
|
14
|
+
}
|
|
6
15
|
function bindingProjection(record) {
|
|
7
16
|
return {
|
|
8
17
|
location: record.location,
|
|
@@ -24,7 +33,7 @@ function bindingProjection(record) {
|
|
|
24
33
|
};
|
|
25
34
|
}
|
|
26
35
|
export async function projectSessionBindings(input) {
|
|
27
|
-
const rows = await input.hostLedger.listPresence({ location: input.location, session: input.sessionId,
|
|
36
|
+
const rows = await input.hostLedger.listPresence({ location: input.location, session: input.sessionId, now: input.now });
|
|
28
37
|
return rows.map(bindingProjection);
|
|
29
38
|
}
|
|
30
39
|
export function sessionIdsFromEnvironment(env = process.env) {
|
|
@@ -32,7 +41,7 @@ export function sessionIdsFromEnvironment(env = process.env) {
|
|
|
32
41
|
.map((value) => value?.trim()).filter((value) => Boolean(value));
|
|
33
42
|
}
|
|
34
43
|
export async function projectLocalParticipantBinding(input) {
|
|
35
|
-
const bindings = (await Promise.all(input.sessionIds.map((sessionId) => projectSessionBindings({ hostLedger: input.hostLedger, location: input.location, sessionId,
|
|
44
|
+
const bindings = (await Promise.all(input.sessionIds.map((sessionId) => projectSessionBindings({ hostLedger: input.hostLedger, location: input.location, sessionId, now: input.now })))).flat();
|
|
36
45
|
return bindings.find((binding) => binding.participant.toLocaleLowerCase() === input.participant.toLocaleLowerCase());
|
|
37
46
|
}
|
|
38
47
|
export async function projectPresentation(input) {
|
package/dist/square-storage.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { setTimeout as sleep } from 'node:timers/promises';
|
|
4
3
|
import { isDeepStrictEqual } from 'node:util';
|
|
5
|
-
import { createSquareFile, createSquareState, diagnoseSquareFile as diagnoseArtifactFile, loadSquare, probeSquare, readSquareSnapshot, transactSquareSnapshot, writeSquareFile, } from './artifact.js';
|
|
4
|
+
import { createSquareFile, createSquareState, diagnoseSquareFile as diagnoseArtifactFile, loadSquare, probeSquare, observeSquareRevision, readSquareSnapshot, transactSquareSnapshot, writeSquareFile, } from './artifact.js';
|
|
6
5
|
export { createSquareState };
|
|
7
6
|
async function canonicalPath(squarePath) {
|
|
8
7
|
const absolute = path.resolve(squarePath);
|
|
@@ -49,9 +48,6 @@ function isThenable(value) {
|
|
|
49
48
|
function discardThenable(value) {
|
|
50
49
|
void Promise.resolve(value).catch(() => undefined);
|
|
51
50
|
}
|
|
52
|
-
function isBusy(error) {
|
|
53
|
-
return error instanceof Error && /database is locked|database is busy|SQLITE_BUSY/i.test(error.message);
|
|
54
|
-
}
|
|
55
51
|
/** In-process cell for fast behavior tests and embedded consumers. */
|
|
56
52
|
export function createMemoryCell(initial) {
|
|
57
53
|
let state = cloneState(initial);
|
|
@@ -65,6 +61,7 @@ export function createMemoryCell(initial) {
|
|
|
65
61
|
continue;
|
|
66
62
|
clearTimeout(waiter.timer);
|
|
67
63
|
waiters.delete(waiter);
|
|
64
|
+
waiter.signal?.removeEventListener("abort", waiter.onAbort);
|
|
68
65
|
waiter.resolve(true);
|
|
69
66
|
}
|
|
70
67
|
}
|
|
@@ -106,19 +103,29 @@ export function createMemoryCell(initial) {
|
|
|
106
103
|
throw signal.reason ?? new Error('StateCell operation aborted');
|
|
107
104
|
return { state: cloneState(state), version };
|
|
108
105
|
},
|
|
109
|
-
changed(sinceVersion, timeoutMs) {
|
|
106
|
+
changed(sinceVersion, timeoutMs, signal) {
|
|
110
107
|
assertCellOpen(closed);
|
|
108
|
+
if (signal?.aborted)
|
|
109
|
+
return Promise.reject(signal.reason ?? new Error("StateCell operation aborted"));
|
|
111
110
|
if (version > sinceVersion)
|
|
112
111
|
return Promise.resolve(true);
|
|
113
112
|
if (timeoutMs <= 0)
|
|
114
113
|
return Promise.resolve(false);
|
|
115
|
-
return new Promise((resolve) => {
|
|
114
|
+
return new Promise((resolve, reject) => {
|
|
115
|
+
if (signal?.aborted) {
|
|
116
|
+
reject(signal.reason ?? new Error("StateCell operation aborted"));
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const abort = () => { waiters.delete(waiter); clearTimeout(waiter.timer); signal?.removeEventListener("abort", abort); reject(signal?.reason ?? new Error("StateCell operation aborted")); };
|
|
116
120
|
const waiter = {
|
|
117
121
|
since: sinceVersion,
|
|
118
122
|
resolve,
|
|
119
|
-
timer: setTimeout(() => { waiters.delete(waiter); resolve(false); }, timeoutMs),
|
|
123
|
+
timer: setTimeout(() => { waiters.delete(waiter); signal?.removeEventListener("abort", abort); resolve(false); }, timeoutMs),
|
|
124
|
+
onAbort: abort,
|
|
125
|
+
signal,
|
|
120
126
|
};
|
|
121
127
|
waiters.add(waiter);
|
|
128
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
122
129
|
});
|
|
123
130
|
},
|
|
124
131
|
async close() {
|
|
@@ -129,6 +136,7 @@ export function createMemoryCell(initial) {
|
|
|
129
136
|
for (const waiter of [...waiters]) {
|
|
130
137
|
clearTimeout(waiter.timer);
|
|
131
138
|
waiters.delete(waiter);
|
|
139
|
+
waiter.signal?.removeEventListener("abort", waiter.onAbort);
|
|
132
140
|
waiter.resolve(false);
|
|
133
141
|
}
|
|
134
142
|
},
|
|
@@ -172,36 +180,21 @@ export function createFileCell(squarePath, externalSignal) {
|
|
|
172
180
|
const snapshot = await readSquareSnapshot(await storagePath(), operationSignal(signal));
|
|
173
181
|
return { state: cloneState(snapshot.state), version: snapshot.revision };
|
|
174
182
|
},
|
|
175
|
-
async changed(sinceVersion, timeoutMs) {
|
|
183
|
+
async changed(sinceVersion, timeoutMs, signal) {
|
|
176
184
|
assertCellOpen(closed);
|
|
177
185
|
const deadline = Date.now() + Math.max(0, timeoutMs);
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
return true;
|
|
185
|
-
}
|
|
186
|
-
catch (error) {
|
|
187
|
-
if (closed || cancel.signal.aborted || timeout.aborted)
|
|
188
|
-
return false;
|
|
189
|
-
if (!isBusy(error))
|
|
190
|
-
throw error;
|
|
191
|
-
}
|
|
192
|
-
const nextRemaining = deadline - Date.now();
|
|
193
|
-
if (nextRemaining <= 0)
|
|
186
|
+
const observer = await observeSquareRevision(await storagePath());
|
|
187
|
+
try {
|
|
188
|
+
return await observer.changed(sinceVersion, Math.max(0, deadline - Date.now()), operationSignal(signal));
|
|
189
|
+
}
|
|
190
|
+
catch (error) {
|
|
191
|
+
if (closed || cancel.signal.aborted)
|
|
194
192
|
return false;
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
if (closed || cancel.signal.aborted)
|
|
200
|
-
return false;
|
|
201
|
-
throw error;
|
|
202
|
-
}
|
|
193
|
+
throw error;
|
|
194
|
+
}
|
|
195
|
+
finally {
|
|
196
|
+
observer.close();
|
|
203
197
|
}
|
|
204
|
-
return false;
|
|
205
198
|
},
|
|
206
199
|
async close() {
|
|
207
200
|
if (closed)
|
package/dist/square-wiring.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Activity, ExpressResult, HistoryQuery, ParticipantStatus, SquareSnapshot } from './square-facade.js';
|
|
2
|
-
import type { Participant, SquareAtInput, SquareBuildInput } from './square-facade.js';
|
|
2
|
+
import type { Participant, OperationControl, SquareAtInput, SquareBuildInput } from './square-facade.js';
|
|
3
3
|
export declare class Square {
|
|
4
4
|
readonly location: string;
|
|
5
5
|
private readonly square;
|
|
@@ -8,23 +8,28 @@ export declare class Square {
|
|
|
8
8
|
static at(input: SquareAtInput): Promise<Square>;
|
|
9
9
|
static build(input: SquareBuildInput): Promise<Square>;
|
|
10
10
|
static inMemory(input: Omit<SquareBuildInput, 'path'>): Square;
|
|
11
|
-
join(name: string): Promise<Participant>;
|
|
12
|
-
joinWithActivity(name: string): Promise<{
|
|
11
|
+
join(name: string, control?: OperationControl): Promise<Participant>;
|
|
12
|
+
joinWithActivity(name: string, control?: OperationControl): Promise<{
|
|
13
13
|
readonly participant: Participant;
|
|
14
14
|
readonly activity: Activity | null;
|
|
15
15
|
}>;
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
takeoverWithActivity(name: string, control?: OperationControl): Promise<{
|
|
17
|
+
readonly participant: Participant;
|
|
18
|
+
readonly activities: readonly Activity[];
|
|
19
|
+
}>;
|
|
20
|
+
takeover(name: string, control?: OperationControl): Promise<Participant>;
|
|
21
|
+
doneOwnedSession(name: string, body: string, expectedEpoch: number, control?: OperationControl): Promise<ExpressResult>;
|
|
22
|
+
implicitJoin(name: string, control?: OperationControl): Promise<{
|
|
18
23
|
readonly state: 'joined' | 'active' | 'done';
|
|
19
24
|
readonly participant?: Participant;
|
|
20
25
|
}>;
|
|
21
26
|
participants(): Promise<ParticipantStatus[]>;
|
|
22
27
|
snapshot(): Promise<SquareSnapshot>;
|
|
23
|
-
history(query?: HistoryQuery): Promise<Activity[]>;
|
|
28
|
+
history(query?: HistoryQuery, control?: OperationControl): Promise<Activity[]>;
|
|
24
29
|
recognize(env: NodeJS.ProcessEnv): Promise<Participant | null>;
|
|
25
30
|
close(): Promise<void>;
|
|
26
|
-
reconcileBinding(): Promise<import("./host-ledger.js").ReconcileBindingResult>;
|
|
27
31
|
endOwnedSession(name: string, sessionId: string, expectedEpoch?: number): Promise<ExpressResult | null>;
|
|
32
|
+
retireEndedSessionRoutes(sessionId: string): Promise<void>;
|
|
28
33
|
}
|
|
29
34
|
export declare function markBoundarySeen(squarePath: string, name: string, actIndexes: readonly number[], at?: number): Promise<void>;
|
|
30
35
|
export declare function openParticipant(input: SquareAtInput, name: string): Promise<{
|