@astrosheep/square 0.3.55 → 0.3.56
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/codex-plugin/.codex-plugin/plugin.json +1 -1
- package/dist/boundary-presentation.d.ts +1 -1
- package/dist/boundary-presentation.js +30 -5
- package/dist/codex-hook.d.ts +5 -1
- package/dist/codex-hook.js +38 -5
- package/dist/inbox.d.ts +1 -1
- package/dist/inbox.js +2 -2
- package/dist/notifications.d.ts +1 -1
- package/dist/notifications.js +7 -7
- package/dist/ports.d.ts +4 -2
- package/dist/presentation-operations.js +3 -3
- package/dist/square-file-adapter.d.ts +3 -1
- package/dist/square-file-adapter.js +3 -3
- package/dist/square-storage.d.ts +2 -2
- package/dist/square-storage.js +24 -9
- package/dist/state-cell.d.ts +2 -2
- package/package.json +1 -1
|
@@ -3,4 +3,4 @@ import type { InboxMembership } from './model.js';
|
|
|
3
3
|
export declare function pendingAtBoundary(inbox: InboxMembership[]): InboxMembership[];
|
|
4
4
|
export declare function renderPendingAtBoundary(inbox: InboxMembership[]): string;
|
|
5
5
|
/** Serialize a boundary across hook processes so one stale inbox cannot render twice. */
|
|
6
|
-
export declare function presentPendingAtBoundary<T>(sessionId: string, present: (context: string) => T | Promise<T>, lookup?: (sessionId: string, env?: NodeJS.ProcessEnv) => Promise<InboxMembership[]> | InboxMembership[], env?: NodeJS.ProcessEnv, signal?: AbortSignal): Promise<T | undefined>;
|
|
6
|
+
export declare function presentPendingAtBoundary<T>(sessionId: string, present: (context: string) => T | Promise<T>, lookup?: (sessionId: string, env?: NodeJS.ProcessEnv, signal?: AbortSignal) => Promise<InboxMembership[]> | InboxMembership[], env?: NodeJS.ProcessEnv, signal?: AbortSignal): Promise<T | undefined>;
|
|
@@ -12,6 +12,29 @@ import { presentationSuppressesWake, projectPresentationEvidence } from './squar
|
|
|
12
12
|
import { formatActivityId } from './square-core.js';
|
|
13
13
|
const CONTEXT_MAX = 1200;
|
|
14
14
|
const presentationLocks = new Map();
|
|
15
|
+
/** One settled promise loses to an abort so an in-process presentation wait stays bounded too. */
|
|
16
|
+
async function awaitWithSignal(promise, signal) {
|
|
17
|
+
if (signal === undefined)
|
|
18
|
+
return promise;
|
|
19
|
+
let onAbort;
|
|
20
|
+
try {
|
|
21
|
+
await Promise.race([
|
|
22
|
+
promise,
|
|
23
|
+
new Promise((_, reject) => {
|
|
24
|
+
if (signal.aborted) {
|
|
25
|
+
reject(signal.reason ?? new Error('Presentation aborted'));
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
onAbort = () => reject(signal.reason ?? new Error('Presentation aborted'));
|
|
29
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
30
|
+
}),
|
|
31
|
+
]);
|
|
32
|
+
}
|
|
33
|
+
finally {
|
|
34
|
+
if (onAbort !== undefined)
|
|
35
|
+
signal.removeEventListener('abort', onAbort);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
15
38
|
function pendingCount(inbox) {
|
|
16
39
|
return inbox.reduce((total, membership) => total + membership.notifications.length, 0);
|
|
17
40
|
}
|
|
@@ -71,7 +94,7 @@ function renderBoundary(inbox) {
|
|
|
71
94
|
};
|
|
72
95
|
}
|
|
73
96
|
async function presentPendingAtBoundaryUnlocked(sessionId, present, lookup = sessionInbox, env = process.env, signal) {
|
|
74
|
-
const inbox = await lookup(sessionId, env);
|
|
97
|
+
const inbox = await lookup(sessionId, env, signal);
|
|
75
98
|
if (signal?.aborted)
|
|
76
99
|
return undefined;
|
|
77
100
|
const hostLedger = hostLedgerForEnv(env);
|
|
@@ -97,7 +120,7 @@ async function presentPendingAtBoundaryUnlocked(sessionId, present, lookup = ses
|
|
|
97
120
|
let square;
|
|
98
121
|
try {
|
|
99
122
|
try {
|
|
100
|
-
square = await openSquare(entry.membership.squarePath, { env });
|
|
123
|
+
square = await openSquare(entry.membership.squarePath, { env, signal });
|
|
101
124
|
}
|
|
102
125
|
catch {
|
|
103
126
|
continue;
|
|
@@ -106,16 +129,18 @@ async function presentPendingAtBoundaryUnlocked(sessionId, present, lookup = ses
|
|
|
106
129
|
const key = `${entry.membership.squarePath}\u0000${entry.membership.name.toLocaleLowerCase()}\u0000${index}`;
|
|
107
130
|
const prior = presentationLocks.get(key);
|
|
108
131
|
if (prior !== undefined) {
|
|
109
|
-
await prior;
|
|
132
|
+
await awaitWithSignal(prior, signal);
|
|
133
|
+
if (signal?.aborted)
|
|
134
|
+
return undefined;
|
|
110
135
|
if (!rendered)
|
|
111
136
|
return undefined;
|
|
112
137
|
continue;
|
|
113
138
|
}
|
|
114
|
-
const work = presentPending({ artifact: square.artifact, location: entry.membership.squarePath, participant: entry.membership.name, activity: index, hostLedger: square.hostLedger, session: sessionId, sink: { present: renderOnce }, markSeen: entry.markSeen, now: Date.now() }).then(async (outcome) => {
|
|
139
|
+
const work = presentPending({ artifact: square.artifact, location: entry.membership.squarePath, participant: entry.membership.name, activity: index, hostLedger: square.hostLedger, session: sessionId, sink: { present: renderOnce }, markSeen: entry.markSeen, now: Date.now(), signal }).then(async (outcome) => {
|
|
115
140
|
// A stale projection can outlive its activity; still surface the bounded preview,
|
|
116
141
|
// but there is no artifact observation to commit.
|
|
117
142
|
if (!outcome.presented) {
|
|
118
|
-
const snapshot = await square.artifact.read().catch(() => undefined);
|
|
143
|
+
const snapshot = await square.artifact.read(signal).catch(() => undefined);
|
|
119
144
|
if (snapshot?.state.acts.every((activity) => activity.index !== index))
|
|
120
145
|
await renderOnce();
|
|
121
146
|
}
|
package/dist/codex-hook.d.ts
CHANGED
|
@@ -6,6 +6,10 @@ export interface CodexHookInput {
|
|
|
6
6
|
cwd?: unknown;
|
|
7
7
|
source?: unknown;
|
|
8
8
|
}
|
|
9
|
-
export
|
|
9
|
+
export type HookTrace = (line: string) => void;
|
|
10
|
+
/** Overall hook budget: Codex kills the hook at its configured 5s timeout, so the hook must
|
|
11
|
+
* bound every wait/retry itself and exit with a readable stage trace before that. */
|
|
12
|
+
export declare const CODEX_HOOK_BUDGET_MS = 4500;
|
|
13
|
+
export declare function codexHookResponse(input: CodexHookInput, lookup?: (sessionId: string, env?: NodeJS.ProcessEnv, signal?: AbortSignal) => Promise<InboxMembership[]> | InboxMembership[], env?: NodeJS.ProcessEnv, deliveryAdapters?: WakeAdapter[], trace?: HookTrace): Promise<object | undefined>;
|
|
10
14
|
export declare function runCodexHook(inputText: string, env?: NodeJS.ProcessEnv): Promise<string>;
|
|
11
15
|
export declare function runCodexHookAsync(inputText: string, env?: NodeJS.ProcessEnv): Promise<string>;
|
package/dist/codex-hook.js
CHANGED
|
@@ -3,12 +3,30 @@ import { sessionInbox } from './inbox.js';
|
|
|
3
3
|
import { automaticSessionEnd, automaticSessionStart } from './automatic-session.js';
|
|
4
4
|
import { clearCodexBoundary, recordCodexBoundary } from './codex-boundary-state.js';
|
|
5
5
|
import { PRIVILEGED_HOOK_BUDGET_MS, sweepPrivilegedPending } from './notifications.js';
|
|
6
|
+
/** Overall hook budget: Codex kills the hook at its configured 5s timeout, so the hook must
|
|
7
|
+
* bound every wait/retry itself and exit with a readable stage trace before that. */
|
|
8
|
+
export const CODEX_HOOK_BUDGET_MS = 4500;
|
|
6
9
|
const CODEX_HOOK_EVENTS = {
|
|
7
10
|
PostToolUse: 'PostToolUse',
|
|
8
11
|
Stop: 'Stop',
|
|
9
12
|
};
|
|
10
|
-
|
|
11
|
-
|
|
13
|
+
const defaultTrace = (line) => { process.stderr.write(`${line}\n`); };
|
|
14
|
+
/** Per-stage timings, emitted as each stage completes so a killed hook still leaves a trace. */
|
|
15
|
+
function stageTracer(trace) {
|
|
16
|
+
const startedAt = Date.now();
|
|
17
|
+
let mark = startedAt;
|
|
18
|
+
return (stage, extra = '') => {
|
|
19
|
+
const now = Date.now();
|
|
20
|
+
const durationMs = now - mark;
|
|
21
|
+
mark = now;
|
|
22
|
+
trace(`square-codex-hook: stage=${stage} durationMs=${durationMs} totalMs=${now - startedAt}${extra === '' ? '' : ` ${extra}`}`);
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function hookBudgetMs(env) {
|
|
26
|
+
const configured = Number(env.SQUARE_CODEX_HOOK_BUDGET_MS ?? CODEX_HOOK_BUDGET_MS);
|
|
27
|
+
return Number.isFinite(configured) && configured > 0 ? configured : CODEX_HOOK_BUDGET_MS;
|
|
28
|
+
}
|
|
29
|
+
export async function codexHookResponse(input, lookup = sessionInbox, env = process.env, deliveryAdapters, trace = defaultTrace) {
|
|
12
30
|
if (typeof input.session_id !== 'string' || input.session_id === '')
|
|
13
31
|
return undefined;
|
|
14
32
|
if (typeof input.hook_event_name !== 'string')
|
|
@@ -16,13 +34,28 @@ export async function codexHookResponse(input, lookup = sessionInbox, env = proc
|
|
|
16
34
|
const hookEventName = CODEX_HOOK_EVENTS[input.hook_event_name];
|
|
17
35
|
if (hookEventName === undefined)
|
|
18
36
|
return undefined;
|
|
37
|
+
const stage = stageTracer(trace);
|
|
38
|
+
const signal = AbortSignal.timeout(hookBudgetMs(env));
|
|
39
|
+
const sweepDeadline = Date.now() + Math.min(PRIVILEGED_HOOK_BUDGET_MS, hookBudgetMs(env));
|
|
40
|
+
const cwd = typeof input.cwd === 'string' ? input.cwd : process.cwd();
|
|
19
41
|
await recordCodexBoundary(input.session_id, hookEventName === 'Stop' ? 'Stop' : 'non-stop', env);
|
|
42
|
+
stage('boundary-record');
|
|
20
43
|
if (hookEventName === 'Stop') {
|
|
21
|
-
await sweepPrivilegedPending(
|
|
44
|
+
await sweepPrivilegedPending(cwd, env, deliveryAdapters, sweepDeadline, signal).catch(() => undefined);
|
|
45
|
+
stage('sweep', signal.aborted ? 'aborted=true' : '');
|
|
22
46
|
return undefined;
|
|
23
47
|
}
|
|
24
|
-
|
|
25
|
-
|
|
48
|
+
let response;
|
|
49
|
+
try {
|
|
50
|
+
response = await presentPendingAtBoundary(input.session_id, (context) => ({ hookSpecificOutput: { hookEventName: 'PostToolUse', additionalContext: context } }), lookup, env, signal);
|
|
51
|
+
stage('presentation', signal.aborted ? 'aborted=true' : '');
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
stage('presentation', `error=${error instanceof Error ? error.name : String(error)}`);
|
|
55
|
+
response = undefined;
|
|
56
|
+
}
|
|
57
|
+
await sweepPrivilegedPending(cwd, env, deliveryAdapters, sweepDeadline, signal).catch(() => undefined);
|
|
58
|
+
stage('sweep', signal.aborted ? 'aborted=true' : '');
|
|
26
59
|
return response;
|
|
27
60
|
}
|
|
28
61
|
export async function runCodexHook(inputText, env = process.env) {
|
package/dist/inbox.d.ts
CHANGED
|
@@ -9,6 +9,6 @@ export interface PendingWaitOptions {
|
|
|
9
9
|
/** Reports whether a change wait has been established for a deferred retry. */
|
|
10
10
|
onChangeArmed?: (armed: boolean) => void;
|
|
11
11
|
}
|
|
12
|
-
export declare function sessionInbox(sessionId: string, env?: NodeJS.ProcessEnv): Promise<InboxMembership[]>;
|
|
12
|
+
export declare function sessionInbox(sessionId: string, env?: NodeJS.ProcessEnv, signal?: AbortSignal): Promise<InboxMembership[]>;
|
|
13
13
|
/** Wait for a bound square to produce a new pending notification without consuming it. */
|
|
14
14
|
export declare function waitForSessionPending(sessionId: string, timeoutMs: number, options?: PendingWaitOptions, env?: NodeJS.ProcessEnv): Promise<InboxMembership[]>;
|
package/dist/inbox.js
CHANGED
|
@@ -24,13 +24,13 @@ function withoutExcluded(inbox, excludeKeys) {
|
|
|
24
24
|
}))
|
|
25
25
|
.filter((membership) => membership.notifications.length > 0);
|
|
26
26
|
}
|
|
27
|
-
export async function sessionInbox(sessionId, env = process.env) {
|
|
27
|
+
export async function sessionInbox(sessionId, env = process.env, signal) {
|
|
28
28
|
const inbox = [];
|
|
29
29
|
const hostLedger = hostLedgerForEnv(env);
|
|
30
30
|
for (const binding of await projectSessionBindings({ hostLedger, sessionId })) {
|
|
31
31
|
let square;
|
|
32
32
|
try {
|
|
33
|
-
square = await openSquare(binding.location, { env, hostLedger });
|
|
33
|
+
square = await openSquare(binding.location, { env, hostLedger, signal });
|
|
34
34
|
const projection = await projectPresentation({ artifact: square.artifact, binding, now: square.clock() });
|
|
35
35
|
if (!projection.joined)
|
|
36
36
|
continue;
|
package/dist/notifications.d.ts
CHANGED
|
@@ -23,7 +23,7 @@ export declare function createDefaultWakeTransport(hostLedger: import('./host-le
|
|
|
23
23
|
export declare function createWakeTransport(adapters: readonly WakeAdapter[], hostLedger: import('./host-ledger.js').HostLedgerPort, clock: () => number): WakeTransportPort;
|
|
24
24
|
export declare function processActNotificationsOnce(squarePath: string, actIndex: number, opts?: ProcessNotificationOptions): Promise<import("./ports.js").DeliveryResult>;
|
|
25
25
|
/** Privileged hook fallback: sweep indexed squares plus the current cwd's local squares within one boundary budget. */
|
|
26
|
-
export declare function sweepPrivilegedPending(cwd: string, env?: NodeJS.ProcessEnv, suppliedAdapters?: WakeAdapter[], deadline?: number): Promise<void>;
|
|
26
|
+
export declare function sweepPrivilegedPending(cwd: string, env?: NodeJS.ProcessEnv, suppliedAdapters?: WakeAdapter[], deadline?: number, signal?: AbortSignal): Promise<void>;
|
|
27
27
|
export interface SweepPendingNotificationsOptions {
|
|
28
28
|
env?: NodeJS.ProcessEnv;
|
|
29
29
|
now?: number;
|
package/dist/notifications.js
CHANGED
|
@@ -192,9 +192,9 @@ export async function processActNotificationsOnce(squarePath, actIndex, opts = {
|
|
|
192
192
|
}
|
|
193
193
|
}
|
|
194
194
|
/** Privileged hook fallback: sweep indexed squares plus the current cwd's local squares within one boundary budget. */
|
|
195
|
-
export async function sweepPrivilegedPending(cwd, env = process.env, suppliedAdapters, deadline = Date.now() + PRIVILEGED_HOOK_BUDGET_MS) {
|
|
195
|
+
export async function sweepPrivilegedPending(cwd, env = process.env, suppliedAdapters, deadline = Date.now() + PRIVILEGED_HOOK_BUDGET_MS, signal) {
|
|
196
196
|
const remainingMs = () => Math.max(0, deadline - Date.now());
|
|
197
|
-
if (remainingMs() === 0)
|
|
197
|
+
if (remainingMs() === 0 || signal?.aborted)
|
|
198
198
|
return;
|
|
199
199
|
const root = env.SQUARE_REGISTRY === undefined ? undefined : path.dirname(env.SQUARE_REGISTRY);
|
|
200
200
|
const hostLedger = createHostLedgerPort({ userPath: env.SQUARE_HOST_LEDGER_USER ?? root, localPath: env.SQUARE_HOST_LEDGER_LOCAL ?? root, readableScopes: ['user'], writableScope: 'user' });
|
|
@@ -211,17 +211,17 @@ export async function sweepPrivilegedPending(cwd, env = process.env, suppliedAda
|
|
|
211
211
|
}
|
|
212
212
|
}
|
|
213
213
|
catch { /* no local square directory */ }
|
|
214
|
-
if (remainingMs() === 0)
|
|
214
|
+
if (remainingMs() === 0 || signal?.aborted)
|
|
215
215
|
return;
|
|
216
216
|
const adapters = suppliedAdapters ?? await defaultWakeAdapters();
|
|
217
217
|
for (const squarePath of paths) {
|
|
218
|
-
if (remainingMs() === 0)
|
|
218
|
+
if (remainingMs() === 0 || signal?.aborted)
|
|
219
219
|
break;
|
|
220
220
|
try {
|
|
221
|
-
const square = await openSquare(squarePath, { hostLedger, env });
|
|
221
|
+
const square = await openSquare(squarePath, { hostLedger, env, signal });
|
|
222
222
|
try {
|
|
223
223
|
await hostLedger.reconcileBinding({ artifact: square.artifact, scopes: ['user'], now: Date.now() }).catch(() => undefined);
|
|
224
|
-
if (remainingMs() === 0)
|
|
224
|
+
if (remainingMs() === 0 || signal?.aborted)
|
|
225
225
|
break;
|
|
226
226
|
const limit = Number.parseInt(env.SQUARE_NOTIFY_SWEEP_LIMIT ?? '8', 10);
|
|
227
227
|
const graceMs = 0;
|
|
@@ -229,7 +229,7 @@ export async function sweepPrivilegedPending(cwd, env = process.env, suppliedAda
|
|
|
229
229
|
const transport = createWakeTransport(adapters, hostLedger, Date.now);
|
|
230
230
|
for (const actIndex of selected) {
|
|
231
231
|
const remaining = remainingMs();
|
|
232
|
-
if (remaining === 0)
|
|
232
|
+
if (remaining === 0 || signal?.aborted)
|
|
233
233
|
break;
|
|
234
234
|
const configured = Number(env.SQUARE_NOTIFY_DELIVERY_WAIT_MS ?? 5000);
|
|
235
235
|
const timeoutMs = Math.max(1, Math.min(Number.isFinite(configured) && configured > 0 ? configured : 5000, remaining));
|
package/dist/ports.d.ts
CHANGED
|
@@ -4,14 +4,14 @@ export type { ClaimResult, EvidenceClaim, EvidenceRelease, EvidenceGc, EvidenceL
|
|
|
4
4
|
import type { HostLedgerPort } from './host-ledger.js';
|
|
5
5
|
/** Atomic artifact access. Never exposes framing, compression, locks, or storage schema. */
|
|
6
6
|
export interface SquareArtifactPort {
|
|
7
|
-
read(): Promise<{
|
|
7
|
+
read(signal?: AbortSignal): Promise<{
|
|
8
8
|
state: SquareState;
|
|
9
9
|
version: number;
|
|
10
10
|
}>;
|
|
11
11
|
transact<R>(fn: (state: SquareState, version: number) => {
|
|
12
12
|
state?: SquareState;
|
|
13
13
|
result: R;
|
|
14
|
-
}): Promise<R>;
|
|
14
|
+
}, signal?: AbortSignal): Promise<R>;
|
|
15
15
|
changed(sinceVersion: number, timeoutMs: number): Promise<boolean>;
|
|
16
16
|
close(): Promise<void>;
|
|
17
17
|
}
|
|
@@ -134,6 +134,8 @@ export interface PresentPendingInput {
|
|
|
134
134
|
readonly session?: string;
|
|
135
135
|
readonly now?: number;
|
|
136
136
|
readonly timeoutMs?: number;
|
|
137
|
+
/** One hook deadline propagated into artifact reads/transitions so busy retries stay bounded. */
|
|
138
|
+
readonly signal?: AbortSignal;
|
|
137
139
|
/** Boundary previews that were clipped remain pending for a later full presentation. */
|
|
138
140
|
readonly markSeen?: boolean;
|
|
139
141
|
}
|
|
@@ -8,7 +8,7 @@ export async function presentPending(input) {
|
|
|
8
8
|
return { presented: false };
|
|
9
9
|
let before;
|
|
10
10
|
try {
|
|
11
|
-
before = await input.artifact.read();
|
|
11
|
+
before = await input.artifact.read(input.signal);
|
|
12
12
|
}
|
|
13
13
|
catch {
|
|
14
14
|
return { presented: false };
|
|
@@ -25,7 +25,7 @@ export async function presentPending(input) {
|
|
|
25
25
|
claimToken = claim.claimToken;
|
|
26
26
|
let current;
|
|
27
27
|
try {
|
|
28
|
-
current = await input.artifact.read();
|
|
28
|
+
current = await input.artifact.read(input.signal);
|
|
29
29
|
}
|
|
30
30
|
catch {
|
|
31
31
|
await input.hostLedger.releaseEvidence({ location: input.location, participant: input.participant, session: input.session, activity: formatActivityId(index), kind: 'presentation', claimToken }).catch(() => undefined);
|
|
@@ -46,7 +46,7 @@ export async function presentPending(input) {
|
|
|
46
46
|
else
|
|
47
47
|
await input.sink.present(item);
|
|
48
48
|
if (input.markSeen !== false)
|
|
49
|
-
await input.artifact.transact((state) => { const changed = recordObservation(state, input.participant, index, 'seen', input.now ?? Date.now()); return changed ? { state, result: undefined } : { result: undefined }; });
|
|
49
|
+
await input.artifact.transact((state) => { const changed = recordObservation(state, input.participant, index, 'seen', input.now ?? Date.now()); return changed ? { state, result: undefined } : { result: undefined }; }, input.signal);
|
|
50
50
|
if (input.hostLedger !== undefined && input.session !== undefined && claimToken !== undefined)
|
|
51
51
|
await input.hostLedger.appendEvidence({ location: input.location, participant: input.participant, session: input.session, activity: formatActivityId(index), kind: 'presentation', outcome: input.markSeen === false ? 'clipped' : 'presented', ...(input.markSeen === false ? { message: 'presentation clipped' } : {}), claimToken });
|
|
52
52
|
return { presented: true, activity: item };
|
|
@@ -13,8 +13,10 @@ export interface SquareBuildOptions {
|
|
|
13
13
|
hostLedger?: HostLedgerPort;
|
|
14
14
|
wakeTransport?: import('./ports.js').WakeTransportPort;
|
|
15
15
|
env?: NodeJS.ProcessEnv;
|
|
16
|
+
/** One caller deadline (e.g. a native hook budget) propagated into artifact busy retries. */
|
|
17
|
+
signal?: AbortSignal;
|
|
16
18
|
}
|
|
17
|
-
export declare function openSquare(squarePath: string, options?: Pick<SquareBuildOptions, 'clock' | 'hostLedger' | 'wakeTransport' | 'env'>): Promise<OpenSquare>;
|
|
19
|
+
export declare function openSquare(squarePath: string, options?: Pick<SquareBuildOptions, 'clock' | 'hostLedger' | 'wakeTransport' | 'env' | 'signal'>): Promise<OpenSquare>;
|
|
18
20
|
export declare function probeSquare(squarePath: string): Promise<OpenSquare | undefined>;
|
|
19
21
|
export declare function buildSquare(squarePath: string, options: SquareBuildOptions): Promise<OpenSquare>;
|
|
20
22
|
export declare function buildMemorySquare(options: SquareBuildOptions): OpenSquare;
|
|
@@ -27,10 +27,10 @@ function validateBuildOptions(options) {
|
|
|
27
27
|
export async function openSquare(squarePath, options = {}) {
|
|
28
28
|
const env = options.env ?? process.env;
|
|
29
29
|
const ledgerRoot = env.SQUARE_REGISTRY === undefined ? undefined : path.dirname(env.SQUARE_REGISTRY);
|
|
30
|
-
const cell = openSquareCell(squarePath);
|
|
30
|
+
const cell = openSquareCell(squarePath, options.signal);
|
|
31
31
|
try {
|
|
32
32
|
await cell.read();
|
|
33
|
-
const artifact = { read: () => cell.read(), transact: (fn) => cell.transact(fn), changed: (since, timeout) => cell.changed(since, timeout), close: () => cell.close() };
|
|
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() };
|
|
34
34
|
return {
|
|
35
35
|
artifact,
|
|
36
36
|
clock: options.clock ?? Date.now,
|
|
@@ -82,7 +82,7 @@ export function buildMemorySquare(options) {
|
|
|
82
82
|
return { artifact: memoryArtifact(createMemoryCell(squareState)), clock: options.clock ?? Date.now, location: 'memory', hostLedger: options.hostLedger, wakeTransport: options.wakeTransport };
|
|
83
83
|
}
|
|
84
84
|
function memoryArtifact(cell) {
|
|
85
|
-
return { read: () => cell.read(), transact: (fn) => cell.transact(fn), changed: (since, timeout) => cell.changed(since, timeout), close: () => cell.close() };
|
|
85
|
+
return { read: (signal) => cell.read(signal), transact: (fn, signal) => cell.transact(fn, signal), changed: (since, timeout) => cell.changed(since, timeout), close: () => cell.close() };
|
|
86
86
|
}
|
|
87
87
|
export async function waitForSquareChanges(squarePaths, timeoutMs, signal, afterReady, onArmed) {
|
|
88
88
|
if (timeoutMs <= 0 || signal?.aborted || squarePaths.length === 0) {
|
package/dist/square-storage.d.ts
CHANGED
|
@@ -12,6 +12,6 @@ export declare function createSquareSnapshot(squarePath: string, squareState: Sq
|
|
|
12
12
|
/** In-process cell for fast behavior tests and embedded consumers. */
|
|
13
13
|
export declare function createMemoryCell(initial: SquareState): StateCell;
|
|
14
14
|
/** SQLite-backed cell. Revisions are read from the authoritative database, not file metadata. */
|
|
15
|
-
export declare function createFileCell(squarePath: string): StateCell;
|
|
15
|
+
export declare function createFileCell(squarePath: string, externalSignal?: AbortSignal): StateCell;
|
|
16
16
|
/** Consumer-facing file cell factory; keeps SQLite framing behind this module. */
|
|
17
|
-
export declare function openSquareCell(squarePath: string): StateCell;
|
|
17
|
+
export declare function openSquareCell(squarePath: string, signal?: AbortSignal): StateCell;
|
package/dist/square-storage.js
CHANGED
|
@@ -69,10 +69,14 @@ export function createMemoryCell(initial) {
|
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
71
|
return {
|
|
72
|
-
transact(fn) {
|
|
72
|
+
transact(fn, signal) {
|
|
73
73
|
assertCellOpen(closed);
|
|
74
|
+
if (signal?.aborted)
|
|
75
|
+
throw signal.reason ?? new Error('StateCell operation aborted');
|
|
74
76
|
const operation = tail.then(() => {
|
|
75
77
|
assertCellOpen(closed);
|
|
78
|
+
if (signal?.aborted)
|
|
79
|
+
throw signal.reason ?? new Error('StateCell operation aborted');
|
|
76
80
|
const current = cloneState(state);
|
|
77
81
|
const outcome = fn(current, version);
|
|
78
82
|
if (isThenable(outcome)) {
|
|
@@ -92,10 +96,14 @@ export function createMemoryCell(initial) {
|
|
|
92
96
|
tail = operation.then(() => undefined, () => undefined);
|
|
93
97
|
return operation;
|
|
94
98
|
},
|
|
95
|
-
async read() {
|
|
99
|
+
async read(signal) {
|
|
96
100
|
assertCellOpen(closed);
|
|
101
|
+
if (signal?.aborted)
|
|
102
|
+
throw signal.reason ?? new Error('StateCell operation aborted');
|
|
97
103
|
await tail;
|
|
98
104
|
assertCellOpen(closed);
|
|
105
|
+
if (signal?.aborted)
|
|
106
|
+
throw signal.reason ?? new Error('StateCell operation aborted');
|
|
99
107
|
return { state: cloneState(state), version };
|
|
100
108
|
},
|
|
101
109
|
changed(sinceVersion, timeoutMs) {
|
|
@@ -127,7 +135,7 @@ export function createMemoryCell(initial) {
|
|
|
127
135
|
};
|
|
128
136
|
}
|
|
129
137
|
/** SQLite-backed cell. Revisions are read from the authoritative database, not file metadata. */
|
|
130
|
-
export function createFileCell(squarePath) {
|
|
138
|
+
export function createFileCell(squarePath, externalSignal) {
|
|
131
139
|
let closed = false;
|
|
132
140
|
let storage;
|
|
133
141
|
let tail = Promise.resolve();
|
|
@@ -139,22 +147,29 @@ export function createFileCell(squarePath) {
|
|
|
139
147
|
function abortError() {
|
|
140
148
|
return new Error('StateCell is closed');
|
|
141
149
|
}
|
|
150
|
+
/** Close wins over every operation; a per-call deadline bounds busy retries alongside it. */
|
|
151
|
+
function operationSignal(signal) {
|
|
152
|
+
if (signal === undefined && externalSignal === undefined)
|
|
153
|
+
return cancel.signal;
|
|
154
|
+
return AbortSignal.any([cancel.signal, ...(externalSignal === undefined ? [] : [externalSignal]), ...(signal === undefined ? [] : [signal])]);
|
|
155
|
+
}
|
|
142
156
|
return {
|
|
143
|
-
transact(fn) {
|
|
157
|
+
transact(fn, signal) {
|
|
144
158
|
assertCellOpen(closed);
|
|
159
|
+
const opSignal = operationSignal(signal);
|
|
145
160
|
const operation = tail.then(async () => {
|
|
146
161
|
assertCellOpen(closed);
|
|
147
|
-
const result = await transactSquareSnapshot(await storagePath(), fn,
|
|
162
|
+
const result = await transactSquareSnapshot(await storagePath(), fn, opSignal);
|
|
148
163
|
return result.result;
|
|
149
164
|
});
|
|
150
165
|
tail = operation.then(() => undefined, () => undefined);
|
|
151
166
|
return operation;
|
|
152
167
|
},
|
|
153
|
-
async read() {
|
|
168
|
+
async read(signal) {
|
|
154
169
|
assertCellOpen(closed);
|
|
155
170
|
await tail;
|
|
156
171
|
assertCellOpen(closed);
|
|
157
|
-
const snapshot = await readSquareSnapshot(await storagePath(),
|
|
172
|
+
const snapshot = await readSquareSnapshot(await storagePath(), operationSignal(signal));
|
|
158
173
|
return { state: cloneState(snapshot.state), version: snapshot.revision };
|
|
159
174
|
},
|
|
160
175
|
async changed(sinceVersion, timeoutMs) {
|
|
@@ -198,6 +213,6 @@ export function createFileCell(squarePath) {
|
|
|
198
213
|
};
|
|
199
214
|
}
|
|
200
215
|
/** Consumer-facing file cell factory; keeps SQLite framing behind this module. */
|
|
201
|
-
export function openSquareCell(squarePath) {
|
|
202
|
-
return createFileCell(squarePath);
|
|
216
|
+
export function openSquareCell(squarePath, signal) {
|
|
217
|
+
return createFileCell(squarePath, signal);
|
|
203
218
|
}
|
package/dist/state-cell.d.ts
CHANGED
|
@@ -4,8 +4,8 @@ export interface StateCell {
|
|
|
4
4
|
transact<R>(fn: (state: SquareState, version: number) => {
|
|
5
5
|
state?: SquareState;
|
|
6
6
|
result: R;
|
|
7
|
-
}): Promise<R>;
|
|
8
|
-
read(): Promise<{
|
|
7
|
+
}, signal?: AbortSignal): Promise<R>;
|
|
8
|
+
read(signal?: AbortSignal): Promise<{
|
|
9
9
|
state: SquareState;
|
|
10
10
|
version: number;
|
|
11
11
|
}>;
|