@astrosheep/square 0.3.70 → 0.3.71
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/dist/activity-feed.d.ts +4 -5
- package/dist/activity-feed.js +14 -10
- package/dist/artifact.d.ts +26 -33
- package/dist/artifact.js +346 -317
- package/dist/attention-presentation.js +2 -9
- package/dist/automatic-session.js +6 -6
- package/dist/boundary-presentation.js +3 -3
- package/dist/catch-decisions.d.ts +19 -12
- package/dist/catch-decisions.js +32 -68
- package/dist/claude-delivery.js +7 -7
- package/dist/claude-mod.js +18 -7
- package/dist/cli/catch-presentation.d.ts +7 -0
- package/dist/cli/catch-presentation.js +55 -0
- package/dist/cli/context.d.ts +1 -1
- package/dist/cli/context.js +5 -23
- package/dist/cli/express-presentation.d.ts +10 -0
- package/dist/cli/express-presentation.js +196 -0
- package/dist/cli/maintenance-commands.d.ts +4 -0
- package/dist/cli/maintenance-commands.js +29 -6
- package/dist/cli/observation-commands.d.ts +7 -5
- package/dist/cli/observation-commands.js +275 -262
- package/dist/cli/program.d.ts +1 -1
- package/dist/cli/program.js +43 -56
- package/dist/cli/registry.js +2 -1
- package/dist/cli/square-commands.d.ts +3 -9
- package/dist/cli/square-commands.js +134 -255
- package/dist/codex-queue.js +4 -1
- package/dist/decisions.d.ts +26 -74
- package/dist/decisions.js +190 -335
- package/dist/delivery-health.js +13 -22
- package/dist/delivery-operations.d.ts +0 -11
- package/dist/delivery-operations.js +44 -33
- package/dist/delivery.d.ts +33 -32
- package/dist/delivery.js +70 -81
- package/dist/help.js +6 -1
- package/dist/inbox.d.ts +27 -0
- package/dist/inbox.js +55 -18
- package/dist/index.d.ts +2 -4
- package/dist/index.js +1 -1
- package/dist/list.d.ts +7 -1
- package/dist/list.js +49 -122
- package/dist/mcp.d.ts +1 -1
- package/dist/mcp.js +7 -37
- package/dist/model.d.ts +4 -44
- package/dist/model.js +1 -3
- package/dist/notifications.d.ts +1 -1
- package/dist/notifications.js +13 -7
- package/dist/open-square.d.ts +16 -2
- package/dist/open-square.js +40 -1
- package/dist/perception-projection.d.ts +6 -15
- package/dist/perception-projection.js +6 -26
- package/dist/plugins/opencode/delivery.js +9 -9
- package/dist/plugins/opencode/index.js +3 -3
- package/dist/ports.d.ts +156 -11
- package/dist/presentation-operations.d.ts +1 -1
- package/dist/presentation-operations.js +35 -44
- package/dist/presentation.d.ts +12 -19
- package/dist/presentation.js +33 -54
- package/dist/routes.d.ts +7 -14
- package/dist/routes.js +33 -62
- package/dist/runtime.d.ts +12 -39
- package/dist/runtime.js +33 -164
- package/dist/square-actions.d.ts +11 -21
- package/dist/square-actions.js +171 -271
- package/dist/square-application.d.ts +31 -55
- package/dist/square-application.js +637 -141
- package/dist/square-core.d.ts +3 -94
- package/dist/square-core.js +8 -353
- package/dist/square-domain.d.ts +24 -0
- package/dist/square-domain.js +77 -0
- package/dist/square-facade.d.ts +360 -9
- package/dist/square-migration.d.ts +24 -0
- package/dist/square-migration.js +211 -0
- package/dist/square-projections.d.ts +2 -42
- package/dist/square-projections.js +1 -37
- package/dist/square-sqlite.d.ts +59 -0
- package/dist/square-sqlite.js +702 -0
- package/dist/stream.d.ts +3 -10
- package/dist/stream.js +81 -52
- package/dist/views.d.ts +27 -95
- package/dist/views.js +217 -87
- package/dist/wake-eligibility.d.ts +15 -5
- package/dist/wake-eligibility.js +32 -16
- package/dist/wakes.d.ts +3 -3
- package/dist/wakes.js +31 -27
- package/guides/application-facade-delivery-evidence.md +200 -0
- package/guides/application-facade-design.md +95 -0
- package/guides/change-waits.md +4 -4
- package/guides/migration.md +77 -0
- package/guides/primitive-catch-consolidation.md +85 -0
- package/guides/primitive-history-consolidation.md +76 -0
- package/guides/primitive-status-consolidation.md +87 -0
- package/guides/primitive-storage-consolidation.md +128 -0
- package/guides/row-runtime-delivery-evidence.md +160 -0
- package/guides/row-storage-design.md +567 -0
- package/package.json +1 -1
- package/plugins/claude/.claude-plugin/plugin.json +1 -1
- package/plugins/codex/.codex-plugin/plugin.json +1 -1
- package/plugins/opencode/delivery.ts +11 -11
- package/plugins/opencode/index.ts +3 -3
- package/plugins/paseo/client/square-message.tsx +70 -52
- package/plugins/paseo/shared/message.ts +15 -0
- package/plugins/pi/square-pi.js +3 -0
- package/dist/activity.d.ts +0 -14
- package/dist/activity.js +0 -248
- package/dist/square-file-adapter.d.ts +0 -26
- package/dist/square-file-adapter.js +0 -85
- package/dist/square-state.d.ts +0 -6
- package/dist/square-state.js +0 -138
- package/dist/square-storage.d.ts +0 -14
- package/dist/square-storage.js +0 -188
- package/dist/square-wiring.d.ts +0 -33
- package/dist/square-wiring.js +0 -109
- package/dist/watch.d.ts +0 -8
- package/dist/watch.js +0 -252
|
@@ -1,185 +1,681 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
+
import crypto from 'node:crypto';
|
|
2
3
|
import { setTimeout as sleep } from 'node:timers/promises';
|
|
3
|
-
import { SquareError } from './model.js';
|
|
4
|
+
import { SquareError, isSquareError, nameKey, validateName } from './model.js';
|
|
4
5
|
import { validateDoneBody } from './decisions.js';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
6
|
+
import { HELD_WAIT_BUDGET_MS, WATCH_HEARTBEAT_MS, inSquareCount, presenceAnchor } from './runtime.js';
|
|
7
|
+
import { catchLimit, observeCatchSelection } from './catch-decisions.js';
|
|
8
|
+
import { directedSuffix } from './activity-feed.js';
|
|
9
|
+
import { assertLiveOwner } from './participant-host.js';
|
|
10
|
+
import { buildMemorySquare, buildSquare, openSquare, closeOpenSquare } from './open-square.js';
|
|
11
|
+
import { catchUp, done, endOwnedSession, express, hold, ignore, implicitJoin, join, takeover, listen, listening, resume, retireEndedSessionRoutes } from './square-actions.js';
|
|
12
|
+
import { recoveryObservation, currentParticipant, entryPresentation, eventPresentation, history, participants, resolveParticipant, snapshot, queryStatus, queryParticipants, catchPresence, toPublicActivity, publicActivities, selectHistory, NON_READ, HISTORY_DEFAULT_LIMIT, HISTORY_MAX_LIMIT, } from './views.js';
|
|
13
|
+
import { hasAutomaticDeliveryIdentity, hostLedgerForEnv, localParticipantName, localSessionIdentities, presenceEpoch, readParticipantOwner } from './registry.js';
|
|
14
|
+
import { projectSessionBindings } from './square-projections.js';
|
|
15
|
+
import { sessionIdsFromEnvironment } from './participant-identity.js';
|
|
16
|
+
import { createDefaultWakeTransport, wakeGraceMs } from './notifications.js';
|
|
17
|
+
import { sweepPending } from './delivery-operations.js';
|
|
18
|
+
import { acquireWatchLease, ownsWatchLease, pulseWatchLease, releaseWatchLease } from './wakes.js';
|
|
19
|
+
import { createSquare, diagnoseSquareFile } from './artifact.js';
|
|
20
|
+
import { migrateSquareFile } from './square-migration.js';
|
|
21
|
+
import { discoverSquares, DEFAULT_LIST_DEPTH, DEFAULT_LIST_LIMIT } from './list.js';
|
|
22
|
+
import { streamSquare } from './stream.js';
|
|
23
|
+
export { formatActivityId, parseActivityId, MAX_IDENTITY_SET_SIZE } from './square-core.js';
|
|
24
|
+
export { CATCH_DEFAULT_LIMIT, CATCH_MAX_LIMIT } from './catch-decisions.js';
|
|
25
|
+
export { HELD_WAIT_BUDGET_MS } from './runtime.js';
|
|
26
|
+
export { HISTORY_DEFAULT_LIMIT, HISTORY_MAX_LIMIT } from './views.js';
|
|
27
|
+
export { DEFAULT_LIST_DEPTH, MAX_LIST_DEPTH, DEFAULT_LIST_LIMIT, MAX_LIST_LIMIT } from './list.js';
|
|
27
28
|
function checkControl(control) {
|
|
28
29
|
if (control?.signal?.aborted)
|
|
29
|
-
throw new
|
|
30
|
+
throw control.signal.reason ?? new Error('Square operation was cancelled');
|
|
31
|
+
}
|
|
32
|
+
function clockFor(env, clock) {
|
|
33
|
+
if (clock !== undefined)
|
|
34
|
+
return clock;
|
|
35
|
+
if (env.SQUARE_NOW_MS === undefined)
|
|
36
|
+
return Date.now;
|
|
37
|
+
return () => {
|
|
38
|
+
const value = Number.parseInt(env.SQUARE_NOW_MS, 10);
|
|
39
|
+
if (!Number.isFinite(value))
|
|
40
|
+
throw new SquareError('invalid_args', 'Invalid SQUARE_NOW_MS: expected integer milliseconds.');
|
|
41
|
+
return value;
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function failure(error, detail) {
|
|
45
|
+
const normalized = error instanceof Error ? error : new Error(String(error), { cause: error });
|
|
46
|
+
Object.assign(normalized, { operation: detail });
|
|
47
|
+
return normalized;
|
|
48
|
+
}
|
|
49
|
+
function defaultTransport(square, inMemory = false) {
|
|
50
|
+
let transport;
|
|
51
|
+
return () => transport ??= square.wakeTransport !== undefined ? Promise.resolve(square.wakeTransport)
|
|
52
|
+
: inMemory || square.hostLedger === undefined ? Promise.resolve(undefined)
|
|
53
|
+
: createDefaultWakeTransport(square.hostLedger, square.clock, square.env).catch(() => undefined);
|
|
54
|
+
}
|
|
55
|
+
function observedContext(square, standingCount, held, participant, now = square.clock()) {
|
|
56
|
+
return {
|
|
57
|
+
location: square.location, standingCount, held, now,
|
|
58
|
+
...(participant === undefined ? {} : { participant }),
|
|
59
|
+
automaticDeliveryAvailable: hasAutomaticDeliveryIdentity(square.env),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
async function contextProjection(square, participant, control) {
|
|
63
|
+
const observation = (await square.artifact.read((facts) => ({
|
|
64
|
+
standingCount: inSquareCount(facts), held: facts.hold().active,
|
|
65
|
+
}), control?.signal)).value;
|
|
66
|
+
return observedContext(square, observation.standingCount, observation.held, participant);
|
|
67
|
+
}
|
|
68
|
+
async function joinOwned(square, requested, options = {}, control) {
|
|
69
|
+
checkControl(control);
|
|
70
|
+
if (options.takeover && await currentParticipant(square, requested, control) !== undefined) {
|
|
71
|
+
const result = await takeover(square, requested, [], control);
|
|
72
|
+
return {
|
|
73
|
+
participant: result.name, activity: result.activities.at(-1) ?? null,
|
|
74
|
+
kind: 'taken-over', participantCount: result.participantCount,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
const result = await join(square, requested, control);
|
|
78
|
+
return {
|
|
79
|
+
participant: result.name, activity: result.activity,
|
|
80
|
+
kind: result.reconnected ? 'reconnected' : 'joined', participantCount: result.participantCount,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
async function prepareExpression(square, requested, control) {
|
|
84
|
+
try {
|
|
85
|
+
checkControl(control);
|
|
86
|
+
const name = (await resolveParticipant(square, requested, control)).name;
|
|
87
|
+
return await joinOwned(square, name, {}, control);
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
throw failure(error, { operation: 'express', subject: 'caller', participant: requested, expressEffect: 'unsent' });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/** The one retry/commit owner, shared by application and SDK without projections. */
|
|
94
|
+
async function landExpression(square, name, body, options, transport, control) {
|
|
95
|
+
let effect = 'unsent';
|
|
96
|
+
let waited = false;
|
|
97
|
+
let heldWaitedMs = 0;
|
|
98
|
+
try {
|
|
99
|
+
const wakeTransport = await transport();
|
|
100
|
+
while (true) {
|
|
101
|
+
checkControl(control);
|
|
102
|
+
try {
|
|
103
|
+
effect = 'unknown';
|
|
104
|
+
const result = await express({ ...square, wakeTransport }, name, body, options, control);
|
|
105
|
+
return { ...result, waited };
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
if (isSquareError(error) && error.facts?.activityUnsent === true)
|
|
109
|
+
effect = 'unsent';
|
|
110
|
+
if (!isSquareError(error) || options.noWait || (error.code !== 'held' && error.code !== 'throttled'))
|
|
111
|
+
throw error;
|
|
112
|
+
if (error.code === 'held' && heldWaitedMs >= HELD_WAIT_BUDGET_MS)
|
|
113
|
+
throw error;
|
|
114
|
+
waited = true;
|
|
115
|
+
control?.onProgress?.({
|
|
116
|
+
kind: 'waiting', reason: error.code,
|
|
117
|
+
...(error.code === 'throttled' ? { delayMs: error.facts?.retryAfterMs ?? 1 } : {}),
|
|
118
|
+
...(error.facts?.holder === undefined ? {} : { holder: error.facts.holder }),
|
|
119
|
+
...(error.facts?.holdReason === undefined ? {} : { holdReason: error.facts.holdReason }),
|
|
120
|
+
});
|
|
121
|
+
if (error.code === 'throttled') {
|
|
122
|
+
await sleep(error.facts?.retryAfterMs ?? 1, undefined, { signal: control?.signal });
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
const started = Date.now();
|
|
126
|
+
const observation = await square.artifact.read((facts) => facts.hold().active, control?.signal);
|
|
127
|
+
if (observation.value) {
|
|
128
|
+
await square.artifact.changed(observation.version, Math.max(1, HELD_WAIT_BUDGET_MS - heldWaitedMs), control?.signal);
|
|
129
|
+
}
|
|
130
|
+
heldWaitedMs += Date.now() - started;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
const recipient = isSquareError(error)
|
|
137
|
+
&& ['not_standing', 'unmatched_mention', 'bell_mention_conflict'].includes(error.facts?.reason ?? '');
|
|
138
|
+
throw failure(error, {
|
|
139
|
+
operation: 'express', participant: name, subject: recipient ? 'recipient' : 'caller', expressEffect: effect,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
async function expressOwned(square, requested, body, options, transport, control) {
|
|
144
|
+
const entry = await prepareExpression(square, requested, control);
|
|
145
|
+
const name = entry.participant;
|
|
146
|
+
let result;
|
|
147
|
+
try {
|
|
148
|
+
const feed = (await square.artifact.read((facts) => {
|
|
149
|
+
const pendingPublic = directedSuffix(facts, name);
|
|
150
|
+
return {
|
|
151
|
+
pendingPublic,
|
|
152
|
+
sayNumbers: facts.sayOrdinals(pendingPublic.map((item) => item.index)),
|
|
153
|
+
perceptions: Object.fromEntries(pendingPublic.map((item) => [item.index, 'full'])),
|
|
154
|
+
};
|
|
155
|
+
}, control?.signal)).value;
|
|
156
|
+
result = await landExpression(square, name, body, options, transport, control);
|
|
157
|
+
// Minimal count/header facts come from the authoritative commit, not a later
|
|
158
|
+
// read that could erase success or a second feed that would be discarded.
|
|
159
|
+
const fresh = result.observation;
|
|
160
|
+
return {
|
|
161
|
+
data: {
|
|
162
|
+
activity: result.activity, waited: result.waited,
|
|
163
|
+
...(result.replyAuthorNotHere === undefined ? {} : { replyAuthorNotHere: result.replyAuthorNotHere }),
|
|
164
|
+
},
|
|
165
|
+
projection: {
|
|
166
|
+
...fresh, ...feed, participant: name,
|
|
167
|
+
location: square.location, now: square.clock(), automaticDeliveryAvailable: hasAutomaticDeliveryIdentity(square.env),
|
|
168
|
+
...(entry.activity === null ? {} : { reentry: entry.activity }),
|
|
169
|
+
...(result.delivery === undefined ? {} : { delivery: result.delivery }),
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
if (result === undefined && error instanceof Error && 'operation' in error)
|
|
175
|
+
throw error;
|
|
176
|
+
throw failure(error, {
|
|
177
|
+
operation: 'express', participant: name, subject: 'caller',
|
|
178
|
+
expressEffect: result === undefined ? 'unsent' : 'landed',
|
|
179
|
+
...(result === undefined ? {} : { landedId: result.activity.id }),
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
/** Directed selection guarantees full visibility; only the API exits expose activities. */
|
|
184
|
+
function caughtResult({ selection, stop }) {
|
|
185
|
+
return {
|
|
186
|
+
activities: selection.activities.map((activity) => ({ ...toPublicActivity(activity), perception: 'full' })),
|
|
187
|
+
consumedThrough: selection.consumedThrough, remaining: selection.remaining, idleExpired: stop === 'stale',
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
/** Leases, quiet waiting, selection and sweep form one lifecycle for both APIs. */
|
|
191
|
+
async function catchOwned(square, requested, options = {}, control) {
|
|
192
|
+
let name = requested, subject = 'caller';
|
|
193
|
+
let leaseId;
|
|
194
|
+
let waitStarted = false, replaced = false;
|
|
195
|
+
let selection = options;
|
|
196
|
+
const idle = options.idle ?? 0;
|
|
197
|
+
async function complete(caught, stop) {
|
|
198
|
+
const selected = caught ?? (await square.artifact.read((facts) => observeCatchSelection(facts, name, [], {
|
|
199
|
+
unseenOnly: true, senders: selection.from,
|
|
200
|
+
...(selection.mention === true ? { attentionOnly: true } : {}),
|
|
201
|
+
}, square.clock()))).value;
|
|
202
|
+
const terminal = stop === 'empty-now' || stop === undefined ? selected.terminalStatus ?? stop : stop;
|
|
203
|
+
if (leaseId !== undefined) {
|
|
204
|
+
await releaseWatchLease(square, name, leaseId).catch(() => undefined);
|
|
205
|
+
leaseId = undefined;
|
|
206
|
+
}
|
|
207
|
+
if (!control?.signal?.aborted && stop !== 'replaced' && stop !== 'already-active'
|
|
208
|
+
&& (waitStarted || selected.activities.length > 0) && square.location !== 'memory'
|
|
209
|
+
&& square.hostLedger !== undefined && square.env?.SQUARE_DISABLE_PASEO_WAKE !== '1') {
|
|
210
|
+
try {
|
|
211
|
+
await sweepPending({
|
|
212
|
+
artifact: square.artifact, hostLedger: square.hostLedger, location: square.location,
|
|
213
|
+
now: square.clock(), graceMs: wakeGraceMs(square.env), limit: 8,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
catch { /* a completed receipt is not conditional on notification enrichment */ }
|
|
217
|
+
}
|
|
218
|
+
return { selection: selected, stop: terminal, waitStarted, replaced };
|
|
219
|
+
}
|
|
220
|
+
const empty = (stop) => complete(undefined, stop);
|
|
221
|
+
try {
|
|
222
|
+
checkControl(control);
|
|
223
|
+
if (!Number.isFinite(idle) || idle < 0)
|
|
224
|
+
throw new SquareError('invalid_args', 'Catch idle duration must be a non-negative number');
|
|
225
|
+
if (options.id !== undefined
|
|
226
|
+
&& ['idle', 'from', 'mention', 'limit', 'replace'].some((key) => Object.hasOwn(options, key))) {
|
|
227
|
+
throw new SquareError('invalid_args', 'Catch id cannot be combined with idle, from, mention, limit or replace');
|
|
228
|
+
}
|
|
229
|
+
if (Object.hasOwn(options, 'replace') && idle === 0)
|
|
230
|
+
throw new SquareError('invalid_args', 'Catch replacement requires a positive idle duration');
|
|
231
|
+
catchLimit(options.limit);
|
|
232
|
+
name = (await resolveParticipant(square, requested, control)).name;
|
|
233
|
+
subject = 'filter';
|
|
234
|
+
const from = options.from === undefined ? undefined : await Promise.all(options.from.map(async (target) => (await resolveParticipant(square, target, control)).name));
|
|
235
|
+
selection = { ...options, ...(from === undefined ? {} : { from }) };
|
|
236
|
+
subject = 'caller';
|
|
237
|
+
if (idle === 0) {
|
|
238
|
+
const data = await catchUp(square, name, selection, control);
|
|
239
|
+
return await complete(data, data.activities.length === 0 ? 'empty-now' : undefined);
|
|
240
|
+
}
|
|
241
|
+
if (!await assertLiveOwner(square, name)) {
|
|
242
|
+
throw new SquareError('already_joined', `${name} already stands here — another session holds the name`);
|
|
243
|
+
}
|
|
244
|
+
const watch = {
|
|
245
|
+
...(from === undefined ? {} : { participants: [...from] }),
|
|
246
|
+
...(options.mention ? { mention: name } : {}),
|
|
247
|
+
...(options.replace === undefined ? {} : { replace: options.replace }),
|
|
248
|
+
};
|
|
249
|
+
const start = await acquireWatchLease(square, name, crypto.randomUUID(), watch, control?.signal);
|
|
250
|
+
if (start.type === 'active')
|
|
251
|
+
return await empty('already-active');
|
|
252
|
+
leaseId = start.leaseId;
|
|
253
|
+
waitStarted = true;
|
|
254
|
+
replaced = start.replaced;
|
|
255
|
+
control?.onProgress?.({
|
|
256
|
+
kind: 'catch-started', participant: name, idleMs: idle,
|
|
257
|
+
replacementRequested: options.replace ?? false, replaced,
|
|
258
|
+
});
|
|
259
|
+
let quietSince = square.clock();
|
|
260
|
+
let wasHeld = false;
|
|
261
|
+
let nextHeartbeat = start.heartbeatAt + WATCH_HEARTBEAT_MS;
|
|
262
|
+
while (true) {
|
|
263
|
+
checkControl(control);
|
|
264
|
+
// Capture before evaluation: commits during selection cannot be missed.
|
|
265
|
+
const baseline = await square.artifact.revision(control?.signal);
|
|
266
|
+
const pulse = await pulseWatchLease(square, name, leaseId, watch, square.clock() >= nextHeartbeat, control?.signal);
|
|
267
|
+
if (pulse.type === 'replaced')
|
|
268
|
+
return await empty('replaced');
|
|
269
|
+
if (pulse.type === 'terminal')
|
|
270
|
+
return await empty(pulse.status);
|
|
271
|
+
if (pulse.heartbeatAt !== undefined)
|
|
272
|
+
nextHeartbeat = pulse.heartbeatAt + WATCH_HEARTBEAT_MS;
|
|
273
|
+
if (pulse.type === 'sleep') {
|
|
274
|
+
const caught = await catchUp(square, name, selection, control, leaseId);
|
|
275
|
+
if (caught === null)
|
|
276
|
+
return await empty('replaced');
|
|
277
|
+
if (caught.activities.length > 0)
|
|
278
|
+
return await complete(caught);
|
|
279
|
+
}
|
|
280
|
+
if (pulse.type === 'held' || wasHeld)
|
|
281
|
+
quietSince = square.clock();
|
|
282
|
+
wasHeld = pulse.type === 'held';
|
|
283
|
+
if (!wasHeld && square.clock() - quietSince >= idle) {
|
|
284
|
+
const owns = await ownsWatchLease(square, name, leaseId, control?.signal);
|
|
285
|
+
return await empty(owns ? 'stale' : 'replaced');
|
|
286
|
+
}
|
|
287
|
+
const deadline = Math.min(nextHeartbeat, wasHeld ? Infinity : quietSince + idle);
|
|
288
|
+
await square.artifact.changed(baseline, Math.max(0, deadline - square.clock()), control?.signal);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
catch (error) {
|
|
292
|
+
throw failure(error, { operation: 'catch', subject, participant: name });
|
|
293
|
+
}
|
|
294
|
+
finally {
|
|
295
|
+
// Cleanup deliberately does not reuse the aborted request signal, and the ID
|
|
296
|
+
// fence never releases a replacement lease.
|
|
297
|
+
if (leaseId !== undefined)
|
|
298
|
+
await releaseWatchLease(square, name, leaseId).catch(() => undefined);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
class ParticipantHandle {
|
|
302
|
+
name;
|
|
303
|
+
square;
|
|
304
|
+
transport;
|
|
305
|
+
constructor(name, square, transport) {
|
|
306
|
+
this.name = name;
|
|
307
|
+
this.square = square;
|
|
308
|
+
this.transport = transport;
|
|
309
|
+
}
|
|
310
|
+
async express(body, options, control) {
|
|
311
|
+
const entry = await prepareExpression(this.square, this.name, control);
|
|
312
|
+
const { waited: _waited, observation: _observation, ...result } = await landExpression(this.square, entry.participant, body, { ...options, noWait: true }, this.transport, control);
|
|
313
|
+
return result;
|
|
314
|
+
}
|
|
315
|
+
listen(target, control) { return listen(this.square, this.name, target, control); }
|
|
316
|
+
ignore(target, control) { return ignore(this.square, this.name, target, control); }
|
|
317
|
+
listening(control) { return listening(this.square, this.name, control); }
|
|
318
|
+
async catch(options, control) {
|
|
319
|
+
return caughtResult(await catchOwned(this.square, this.name, options, control));
|
|
320
|
+
}
|
|
321
|
+
history(query = {}, control) {
|
|
322
|
+
return history(this.square, query.limit === undefined ? { ...query, limit: HISTORY_DEFAULT_LIMIT } : query, control);
|
|
323
|
+
}
|
|
324
|
+
hold(reason, control) { return hold(this.square, this.name, reason, control); }
|
|
325
|
+
resume(control) { return resume(this.square, this.name, control); }
|
|
326
|
+
done(body, control) { return done(this.square, this.name, body, {}, control); }
|
|
327
|
+
}
|
|
328
|
+
/** Supported SDK resource. It owns one open artifact; application operations use
|
|
329
|
+
* the same owner functions directly, not an application -> SDK -> application chain. */
|
|
330
|
+
export class Square {
|
|
331
|
+
location;
|
|
332
|
+
square;
|
|
333
|
+
transport;
|
|
334
|
+
constructor(location, square) {
|
|
335
|
+
this.location = location;
|
|
336
|
+
this.square = square;
|
|
337
|
+
this.transport = defaultTransport(square, location === 'memory');
|
|
338
|
+
}
|
|
339
|
+
static async at(input, control) {
|
|
340
|
+
const env = { ...(input.env ?? process.env) };
|
|
341
|
+
return new Square(input.path, await openSquare(input.path, { ...input, env, clock: clockFor(env, input.clock) }, control));
|
|
342
|
+
}
|
|
343
|
+
static async build(input) {
|
|
344
|
+
const env = { ...(input.env ?? process.env) };
|
|
345
|
+
return new Square(input.path, await buildSquare(input.path, { ...input, env, clock: clockFor(env, input.clock) }));
|
|
346
|
+
}
|
|
347
|
+
static inMemory(input) { return new Square('memory', buildMemorySquare(input)); }
|
|
348
|
+
handle(name) { return new ParticipantHandle(name, this.square, this.transport); }
|
|
349
|
+
async join(name, control) { return this.handle((await join(this.square, name, control)).name); }
|
|
350
|
+
async takeover(name, control) {
|
|
351
|
+
const result = await takeover(this.square, name, [], control);
|
|
352
|
+
return this.handle(result.name);
|
|
353
|
+
}
|
|
354
|
+
doneOwnedSession(name, body, expectedEpoch, control) { return done(this.square, name, body, { expectedEpoch }, control); }
|
|
355
|
+
async implicitJoin(name, control) {
|
|
356
|
+
const result = await implicitJoin(this.square, name, control);
|
|
357
|
+
return { state: result.state, ...(result.state === 'done' ? {} : { participant: this.handle(result.name) }) };
|
|
358
|
+
}
|
|
359
|
+
participants() { return participants(this.square); }
|
|
360
|
+
snapshot(control) { return snapshot(this.square, control); }
|
|
361
|
+
history(query, control) { return history(this.square, query, control); }
|
|
362
|
+
async recognize(env) {
|
|
363
|
+
if (this.square.hostLedger === undefined)
|
|
364
|
+
return null;
|
|
365
|
+
const candidates = (await Promise.all(sessionIdsFromEnvironment(env).map((sessionId) => projectSessionBindings({ hostLedger: this.square.hostLedger, location: this.location, sessionId })))).flat();
|
|
366
|
+
if (candidates.length !== 1)
|
|
367
|
+
return null;
|
|
368
|
+
const name = await currentParticipant(this.square, candidates[0].participant);
|
|
369
|
+
return name === undefined ? null : this.handle(name);
|
|
370
|
+
}
|
|
371
|
+
close() { return closeOpenSquare(this.square); }
|
|
372
|
+
endOwnedSession(name, sessionId, expectedEpoch) { return endOwnedSession(this.square, name, sessionId, expectedEpoch); }
|
|
373
|
+
retireEndedSessionRoutes(sessionId) { return retireEndedSessionRoutes(this.square, sessionId); }
|
|
30
374
|
}
|
|
31
375
|
export function createSquareApplication(context) {
|
|
32
376
|
const env = { ...context.env };
|
|
33
|
-
const
|
|
377
|
+
const clock = clockFor(env, context.clock);
|
|
34
378
|
const hostLedger = context.hostLedger ?? hostLedgerForEnv(env);
|
|
35
|
-
const
|
|
36
|
-
|
|
379
|
+
const openOptions = { env, clock, hostLedger, wakeTransport: context.wakeTransport };
|
|
380
|
+
const getTransport = defaultTransport(openOptions);
|
|
381
|
+
function location() {
|
|
382
|
+
const selected = context.squarePath ?? env.SQUARE_LOCATION;
|
|
383
|
+
if (selected === undefined || selected.trim() === '')
|
|
384
|
+
throw new SquareError('invalid_args', 'Square application context needs a square location');
|
|
385
|
+
return path.resolve(context.cwd, selected);
|
|
386
|
+
}
|
|
387
|
+
async function actor() {
|
|
388
|
+
const selected = context.participant ?? env.SQUARE_PARTICIPANT_NAME;
|
|
389
|
+
if (context.participant !== undefined || selected?.trim()) {
|
|
390
|
+
validateName(selected);
|
|
391
|
+
return selected;
|
|
392
|
+
}
|
|
393
|
+
const discovered = await localParticipantName(location(), hostLedger, env);
|
|
394
|
+
if (discovered !== undefined)
|
|
395
|
+
return discovered;
|
|
396
|
+
throw new SquareError('invalid_args', 'Square application context needs one unambiguous participant name');
|
|
397
|
+
}
|
|
398
|
+
async function scoped(operation, control) {
|
|
37
399
|
checkControl(control);
|
|
38
|
-
const
|
|
39
|
-
const participantName = await requireParticipant(context, squarePath, hostLedger, env);
|
|
40
|
-
const square = await openSquare(squarePath, { clock: context.clock, env, hostLedger, wakeTransport: context.wakeTransport }, control);
|
|
400
|
+
const square = await openSquare(location(), openOptions, control);
|
|
41
401
|
try {
|
|
42
|
-
|
|
43
|
-
checkControl(control);
|
|
44
|
-
return await operation(square, name);
|
|
402
|
+
return await operation(square);
|
|
45
403
|
}
|
|
46
404
|
finally {
|
|
47
|
-
await closeOpenSquare(square);
|
|
405
|
+
await closeOpenSquare(square).catch(() => undefined);
|
|
48
406
|
}
|
|
49
407
|
}
|
|
50
|
-
async function
|
|
51
|
-
|
|
52
|
-
const square = await open(control);
|
|
408
|
+
async function selected(operation, run, control) {
|
|
409
|
+
let name = context.participant ?? env.SQUARE_PARTICIPANT_NAME;
|
|
53
410
|
try {
|
|
54
|
-
|
|
411
|
+
checkControl(control);
|
|
412
|
+
name = await actor();
|
|
413
|
+
return await scoped((square) => run(square, name), control);
|
|
55
414
|
}
|
|
56
|
-
|
|
57
|
-
|
|
415
|
+
catch (error) {
|
|
416
|
+
const detail = error?.operation;
|
|
417
|
+
if (detail?.operation === operation)
|
|
418
|
+
throw error;
|
|
419
|
+
throw failure(error, {
|
|
420
|
+
operation, participant: detail?.participant ?? name, subject: 'caller',
|
|
421
|
+
...(operation === 'express' ? { expressEffect: 'unsent' } : {}),
|
|
422
|
+
});
|
|
58
423
|
}
|
|
59
424
|
}
|
|
425
|
+
async function listener(verb, target, control) {
|
|
426
|
+
return selected('other', async (square, requested) => {
|
|
427
|
+
const name = (await resolveParticipant(square, requested, control)).name;
|
|
428
|
+
const result = await (verb === 'listen' ? listen : ignore)(square, name, target, control);
|
|
429
|
+
return {
|
|
430
|
+
data: result,
|
|
431
|
+
projection: {
|
|
432
|
+
...await contextProjection(square, name), participant: name,
|
|
433
|
+
requestedTarget: target, target: result.activity?.target ?? target,
|
|
434
|
+
},
|
|
435
|
+
};
|
|
436
|
+
}, control);
|
|
437
|
+
}
|
|
438
|
+
async function lifecycle(verb, body, control) {
|
|
439
|
+
if (verb === 'done')
|
|
440
|
+
validateDoneBody(body);
|
|
441
|
+
return selected('other', async (square, requested) => {
|
|
442
|
+
const identity = localSessionIdentities(env)[0];
|
|
443
|
+
const owner = verb !== 'done' || identity === undefined ? undefined : await readParticipantOwner(square.location, requested, hostLedger);
|
|
444
|
+
const fenced = identity !== undefined && owner?.session === identity.sessionId && presenceEpoch(owner) > 0;
|
|
445
|
+
const name = fenced ? requested : (await join(square, requested, control)).name;
|
|
446
|
+
let result;
|
|
447
|
+
if (verb === 'done') {
|
|
448
|
+
const fence = fenced ? { expectedEpoch: presenceEpoch(owner) } : {};
|
|
449
|
+
result = await done(square, name, body ?? '', fence, control);
|
|
450
|
+
}
|
|
451
|
+
else if (verb === 'hold') {
|
|
452
|
+
result = await hold(square, name, body, control);
|
|
453
|
+
}
|
|
454
|
+
else {
|
|
455
|
+
result = await resume(square, name, control);
|
|
456
|
+
}
|
|
457
|
+
const view = await eventPresentation(square, result.activity.id);
|
|
458
|
+
return {
|
|
459
|
+
data: { activity: result.activity },
|
|
460
|
+
projection: { ...observedContext(square, view.participantCount, view.held, name), ...view },
|
|
461
|
+
};
|
|
462
|
+
}, control);
|
|
463
|
+
}
|
|
60
464
|
return {
|
|
61
|
-
|
|
465
|
+
now: clock,
|
|
466
|
+
async build(input, control) {
|
|
62
467
|
checkControl(control);
|
|
63
468
|
const squarePath = location();
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
}
|
|
75
|
-
if (options.takeover && standing) {
|
|
76
|
-
const replaced = await square.takeoverWithActivity(participantName, control);
|
|
77
|
-
const activity = replaced.activities.at(-1) ?? null;
|
|
78
|
-
return { participant: replaced.participant.name, activity, kind: 'taken-over', participantCount: (await square.snapshot()).participants.length };
|
|
79
|
-
}
|
|
80
|
-
const joined = await square.joinWithActivity(participantName, control);
|
|
81
|
-
return { participant: joined.participant.name, activity: joined.activity, kind: reconnect ? 'reconnected' : 'joined', participantCount: (await square.snapshot()).participants.length };
|
|
82
|
-
}
|
|
83
|
-
finally {
|
|
84
|
-
await square.close();
|
|
85
|
-
}
|
|
469
|
+
const now = clock();
|
|
470
|
+
const configuration = await createSquare(squarePath, {
|
|
471
|
+
hardCap: input.hardCap, throttlePerMinute: input.throttlePerMinute,
|
|
472
|
+
template: input.template, force: input.force ?? false,
|
|
473
|
+
}, input.markdown, control?.signal);
|
|
474
|
+
return {
|
|
475
|
+
data: { location: squarePath, ...configuration },
|
|
476
|
+
projection: { location: squarePath, standingCount: 0, held: false, now,
|
|
477
|
+
automaticDeliveryAvailable: hasAutomaticDeliveryIdentity(env) },
|
|
478
|
+
};
|
|
86
479
|
},
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
480
|
+
join(options = {}, control) {
|
|
481
|
+
return selected('join', async (square, name) => {
|
|
482
|
+
const last = options.last ?? 10;
|
|
483
|
+
if (!Number.isSafeInteger(last) || last < 1 || last > 100) {
|
|
484
|
+
throw new SquareError('invalid_args', 'Join history limit must be between 1 and 100');
|
|
485
|
+
}
|
|
486
|
+
const data = await joinOwned(square, name, options, control);
|
|
92
487
|
try {
|
|
93
|
-
const
|
|
94
|
-
return {
|
|
488
|
+
const view = await entryPresentation(square, data.participant, last);
|
|
489
|
+
return {
|
|
490
|
+
data,
|
|
491
|
+
projection: { ...observedContext(square, view.participantCount, view.held, data.participant), ...view },
|
|
492
|
+
};
|
|
95
493
|
}
|
|
96
494
|
catch (error) {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
495
|
+
throw failure(error, { operation: 'join', participant: data.participant, subject: 'caller' });
|
|
496
|
+
}
|
|
497
|
+
}, control);
|
|
498
|
+
},
|
|
499
|
+
express(body, options = {}, control) {
|
|
500
|
+
return selected('express', (square, name) => expressOwned(square, name, body, options, getTransport, control), control);
|
|
501
|
+
},
|
|
502
|
+
catch(options, control) {
|
|
503
|
+
return selected('catch', async (square, name) => {
|
|
504
|
+
const result = await catchOwned(square, name, options, control);
|
|
505
|
+
const presence = await catchPresence(square, control).catch(() => undefined);
|
|
506
|
+
const view = result.selection;
|
|
507
|
+
const stop = result.stop;
|
|
508
|
+
const status = stop === 'already-active' || stop === 'replaced' || stop === 'capped' || stop === 'quorum'
|
|
509
|
+
? stop : undefined;
|
|
510
|
+
return {
|
|
511
|
+
data: { ...caughtResult(result), ...(status === undefined ? {} : { status }) },
|
|
512
|
+
projection: {
|
|
513
|
+
...observedContext(square, view.standingCount, view.held, view.participant, view.now),
|
|
514
|
+
participant: view.participant, sayNumbers: view.sayNumbers,
|
|
515
|
+
ownActivityCount: view.ownActivityCount, hardCap: view.hardCap, stop,
|
|
516
|
+
...(presence === undefined ? { enrichmentUnavailable: true } : { presence }),
|
|
517
|
+
waitStarted: result.waitStarted, replaced: result.replaced,
|
|
518
|
+
},
|
|
519
|
+
};
|
|
520
|
+
}, control);
|
|
521
|
+
},
|
|
522
|
+
history(query = {}, control) {
|
|
523
|
+
return scoped(async (square) => {
|
|
524
|
+
if (query.limit !== undefined && (!Number.isSafeInteger(query.limit) || query.limit < 1 || query.limit > HISTORY_MAX_LIMIT)) {
|
|
525
|
+
throw new SquareError('invalid_args', `History limit must be a positive integer no greater than ${HISTORY_MAX_LIMIT}`);
|
|
526
|
+
}
|
|
527
|
+
if (query.grep !== undefined && query.fixed !== undefined)
|
|
528
|
+
throw new SquareError('invalid_args', 'History cannot combine regular-expression and literal search');
|
|
529
|
+
if (query.grep === '' || query.fixed === '')
|
|
530
|
+
throw new SquareError('invalid_args', 'History search requires non-empty text');
|
|
531
|
+
if (query.since !== undefined && !Number.isFinite(query.since))
|
|
532
|
+
throw new SquareError('invalid_args', 'History timestamp must be finite');
|
|
533
|
+
for (const count of [query.contextBefore, query.contextAfter]) {
|
|
534
|
+
if (count !== undefined && (!Number.isSafeInteger(count) || count < 0))
|
|
535
|
+
throw new SquareError('invalid_args', 'History context must be a non-negative safe integer');
|
|
536
|
+
}
|
|
537
|
+
const limit = query.limit ?? (query.at?.length ? null : HISTORY_DEFAULT_LIMIT);
|
|
538
|
+
const kinds = query.at?.length || query.contextBefore !== undefined || query.contextAfter !== undefined || query.since !== undefined ? undefined : NON_READ;
|
|
539
|
+
return (await square.artifact.read((facts) => {
|
|
540
|
+
const selected = selectHistory(facts, query, limit, kinds);
|
|
541
|
+
if (limit === null && selected.totalMatches > HISTORY_MAX_LIMIT) {
|
|
542
|
+
throw failure(new SquareError('invalid_args', `History context exceeds ${HISTORY_MAX_LIMIT} activities`), {
|
|
543
|
+
operation: 'history', historyBounds: { maxLimit: HISTORY_MAX_LIMIT, totalMatches: selected.totalMatches },
|
|
544
|
+
});
|
|
113
545
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
await square.artifact.changed(snapshot.version, Math.max(1, HELD_WAIT_BUDGET_MS - heldWaitedMs), control.signal);
|
|
121
|
-
}
|
|
122
|
-
finally {
|
|
123
|
-
await closeOpenSquare(square);
|
|
124
|
-
}
|
|
125
|
-
heldWaitedMs += Date.now() - waitStarted;
|
|
546
|
+
const indexes = selected.activities.map((activity) => activity.index);
|
|
547
|
+
const presenceAnchors = {};
|
|
548
|
+
for (const participant of facts.roster()) {
|
|
549
|
+
const anchor = presenceAnchor(facts, participant.name);
|
|
550
|
+
if (indexes.includes(anchor))
|
|
551
|
+
(presenceAnchors[anchor] ??= []).push(participant.name);
|
|
126
552
|
}
|
|
127
|
-
|
|
128
|
-
|
|
553
|
+
return {
|
|
554
|
+
data: { activities: publicActivities(selected.activities) },
|
|
555
|
+
projection: {
|
|
556
|
+
...observedContext(square, inSquareCount(facts), facts.hold().active), ...selected,
|
|
557
|
+
sayNumbers: facts.sayOrdinals(indexes), presenceAnchors,
|
|
558
|
+
},
|
|
559
|
+
};
|
|
560
|
+
}, control?.signal)).value;
|
|
561
|
+
}, control);
|
|
562
|
+
},
|
|
563
|
+
listen: (target, control) => listener('listen', target, control),
|
|
564
|
+
ignore: (target, control) => listener('ignore', target, control),
|
|
565
|
+
listening(control) {
|
|
566
|
+
return selected('other', async (square, requested) => {
|
|
567
|
+
const name = (await resolveParticipant(square, requested, control)).name;
|
|
568
|
+
const targets = await listening(square, name, control);
|
|
569
|
+
return { data: { listening: targets }, projection: await contextProjection(square, name, control) };
|
|
570
|
+
}, control);
|
|
129
571
|
},
|
|
130
|
-
|
|
131
|
-
|
|
572
|
+
hold: (reason, control) => lifecycle('hold', reason, control),
|
|
573
|
+
resume: (control) => lifecycle('resume', undefined, control),
|
|
574
|
+
done: (body, control) => lifecycle('done', body, control),
|
|
575
|
+
async status(viewer, control) {
|
|
132
576
|
checkControl(control);
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
checkControl(control);
|
|
138
|
-
return await square.history(query, control);
|
|
139
|
-
}
|
|
140
|
-
finally {
|
|
141
|
-
await square.close();
|
|
142
|
-
}
|
|
577
|
+
const selectedViewer = viewer ?? await actor().catch(() => '');
|
|
578
|
+
return scoped(async (square) => (await square.artifact.read((facts) => queryStatus(facts, {
|
|
579
|
+
location: square.location, now: square.clock(), automaticDeliveryAvailable: hasAutomaticDeliveryIdentity(square.env),
|
|
580
|
+
}, selectedViewer), control?.signal)).value, control);
|
|
143
581
|
},
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
async
|
|
150
|
-
|
|
582
|
+
participants(control) {
|
|
583
|
+
return scoped(async (square) => (await square.artifact.read((facts) => queryParticipants(facts, {
|
|
584
|
+
location: square.location, now: square.clock(), automaticDeliveryAvailable: hasAutomaticDeliveryIdentity(square.env),
|
|
585
|
+
}), control?.signal)).value, control);
|
|
586
|
+
},
|
|
587
|
+
async diagnose(control) {
|
|
588
|
+
checkControl(control);
|
|
151
589
|
const squarePath = location();
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
590
|
+
const data = await diagnoseSquareFile(squarePath);
|
|
591
|
+
return {
|
|
592
|
+
data,
|
|
593
|
+
projection: { location: squarePath, now: clock(),
|
|
594
|
+
...(data.standingCount === undefined ? {} : { standingCount: data.standingCount }) },
|
|
595
|
+
};
|
|
596
|
+
},
|
|
597
|
+
async migrate(control) {
|
|
598
|
+
checkControl(control);
|
|
599
|
+
const squarePath = location();
|
|
600
|
+
const data = await migrateSquareFile(squarePath, control?.signal);
|
|
601
|
+
return { data, projection: { location: squarePath, now: clock(), standingCount: data.standing } };
|
|
602
|
+
},
|
|
603
|
+
async discover(options, control) {
|
|
604
|
+
checkControl(control);
|
|
605
|
+
const data = await discoverSquares(context.cwd, options, control, openOptions);
|
|
606
|
+
return {
|
|
607
|
+
data,
|
|
608
|
+
projection: { cwd: context.cwd, now: clock(),
|
|
609
|
+
depth: options.depth ?? DEFAULT_LIST_DEPTH, limit: options.limit ?? DEFAULT_LIST_LIMIT },
|
|
610
|
+
};
|
|
611
|
+
},
|
|
612
|
+
async inbox(sessionId, options = {}, control) {
|
|
613
|
+
checkControl(control);
|
|
614
|
+
const limit = options.limit ?? 20;
|
|
615
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100)
|
|
616
|
+
throw new SquareError('invalid_args', 'Inbox limit must be between 1 and 100');
|
|
617
|
+
const rows = [];
|
|
618
|
+
for (const binding of await projectSessionBindings({ hostLedger, sessionId })) {
|
|
619
|
+
checkControl(control);
|
|
620
|
+
let square;
|
|
157
621
|
try {
|
|
158
|
-
|
|
622
|
+
square = await openSquare(binding.location, openOptions, control);
|
|
623
|
+
const row = (await square.artifact.read((facts) => {
|
|
624
|
+
const participant = facts.participant(binding.participant);
|
|
625
|
+
if (!participant?.joined)
|
|
626
|
+
return undefined;
|
|
627
|
+
return {
|
|
628
|
+
name: participant.name, squarePath: binding.location,
|
|
629
|
+
pending: facts.directedCount(participant.name, { unseenOnly: true }),
|
|
630
|
+
};
|
|
631
|
+
}, control?.signal)).value;
|
|
632
|
+
if (row !== undefined)
|
|
633
|
+
rows.push(row);
|
|
634
|
+
}
|
|
635
|
+
catch (error) {
|
|
636
|
+
if (control?.signal?.aborted)
|
|
637
|
+
throw error;
|
|
159
638
|
}
|
|
160
639
|
finally {
|
|
161
|
-
|
|
640
|
+
if (square !== undefined)
|
|
641
|
+
await closeOpenSquare(square);
|
|
162
642
|
}
|
|
163
643
|
}
|
|
164
|
-
|
|
644
|
+
rows.sort((a, b) => {
|
|
645
|
+
const compare = (left, right) => left < right ? -1 : left > right ? 1 : 0;
|
|
646
|
+
return compare(a.squarePath, b.squarePath) || compare(nameKey(a.name), nameKey(b.name)) || compare(a.name, b.name);
|
|
647
|
+
});
|
|
648
|
+
return { data: { rows: rows.slice(0, limit), total: rows.length }, projection: { sessionId, now: clock(), limit } };
|
|
165
649
|
},
|
|
166
|
-
async
|
|
167
|
-
|
|
650
|
+
async recovery(control) {
|
|
651
|
+
let projection = { now: clock() };
|
|
168
652
|
try {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
653
|
+
projection = { ...projection, location: location() };
|
|
654
|
+
const name = await actor().catch(() => undefined);
|
|
655
|
+
if (name !== undefined)
|
|
656
|
+
projection = { ...projection, participant: name };
|
|
657
|
+
await scoped(async (square) => {
|
|
658
|
+
if (name === undefined) {
|
|
659
|
+
projection = { ...projection, ...await contextProjection(square, undefined, control) };
|
|
660
|
+
}
|
|
661
|
+
else {
|
|
662
|
+
const facts = await recoveryObservation(square, name, control).catch(() => undefined);
|
|
663
|
+
projection = facts === undefined
|
|
664
|
+
? { ...projection, ...await contextProjection(square, name, control) }
|
|
665
|
+
: {
|
|
666
|
+
...projection, ...observedContext(square, facts.participantCount, facts.held, facts.name),
|
|
667
|
+
holdReason: facts.holdReason, ownActivityCount: facts.ownActivityCount,
|
|
668
|
+
hardCap: facts.hardCap, roster: facts.roster, activitySummaries: facts.activitySummaries,
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
}, control);
|
|
173
672
|
}
|
|
673
|
+
catch { /* unavailable recovery observations never replace the original error */ }
|
|
674
|
+
return { data: projection, projection };
|
|
174
675
|
},
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
return await participantsPresentation(square);
|
|
179
|
-
}
|
|
180
|
-
finally {
|
|
181
|
-
await closeOpenSquare(square);
|
|
182
|
-
}
|
|
676
|
+
stream(options, control) {
|
|
677
|
+
checkControl(control);
|
|
678
|
+
return streamSquare(location(), options, control, openOptions);
|
|
183
679
|
},
|
|
184
680
|
};
|
|
185
681
|
}
|