@astrosheep/square 0.3.77 → 0.3.79
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/attention-presentation.d.ts +3 -1
- package/dist/attention-presentation.js +45 -13
- package/dist/automatic-session.js +1 -1
- package/dist/boundary-presentation.js +4 -6
- package/dist/claude-mod.js +1 -1
- package/dist/cli/catch-presentation.d.ts +2 -2
- package/dist/cli/catch-presentation.js +3 -3
- package/dist/cli/context.d.ts +11 -1
- package/dist/cli/context.js +14 -3
- package/dist/cli/express-presentation.d.ts +6 -5
- package/dist/cli/express-presentation.js +35 -30
- package/dist/cli/maintenance-commands.js +8 -8
- package/dist/cli/mcp-server-command.js +1 -1
- package/dist/cli/observation-commands.d.ts +4 -4
- package/dist/cli/observation-commands.js +20 -16
- package/dist/cli/program.d.ts +1 -1
- package/dist/cli/program.js +13 -7
- package/dist/cli/square-commands.js +35 -23
- package/dist/decisions.d.ts +9 -4
- package/dist/decisions.js +17 -8
- package/dist/delivery.d.ts +1 -1
- package/dist/help.js +15 -2
- package/dist/host-ledger-migration.js +2 -6
- package/dist/host-ledger-sqlite.js +2 -2
- package/dist/inbox.js +3 -3
- package/dist/index.d.ts +1 -2
- package/dist/index.js +1 -2
- package/dist/ledger-maintenance.d.ts +1 -1
- package/dist/ledger-maintenance.js +1 -1
- package/dist/list.d.ts +3 -3
- package/dist/list.js +1 -1
- package/dist/mcp.d.ts +2 -2
- package/dist/mcp.js +12 -7
- package/dist/mention-parse.d.ts +2 -8
- package/dist/mention-parse.js +6 -9
- package/dist/model.d.ts +5 -1
- package/dist/notifications.js +24 -8
- package/dist/participant-host.js +5 -0
- package/dist/plugins/opencode/index.js +1 -1
- package/dist/square-actions.d.ts +22 -9
- package/dist/square-actions.js +27 -19
- package/dist/square-api.d.ts +43 -0
- package/dist/square-api.js +471 -0
- package/dist/square-facade.d.ts +124 -203
- package/dist/square-sqlite.d.ts +2 -8
- package/dist/square-sqlite.js +3 -11
- package/dist/square-tools.d.ts +4 -3
- package/dist/square-tools.js +22 -16
- package/dist/stream.d.ts +2 -2
- package/dist/views.d.ts +16 -12
- package/dist/views.js +13 -27
- package/guides/application-demand-optimization.md +5 -0
- package/guides/application-facade-delivery-evidence.md +6 -1
- package/guides/claude-native.md +3 -1
- package/guides/mcp-connection-lifetime.md +2 -2
- package/guides/mcp.md +1 -1
- package/guides/migration.md +2 -1
- package/guides/operation-cancellation.md +2 -0
- package/guides/primitive-catch-consolidation.md +5 -0
- package/guides/primitive-history-consolidation.md +5 -0
- package/guides/primitive-status-consolidation.md +5 -0
- package/guides/primitive-storage-consolidation.md +5 -0
- package/guides/row-storage-design.md +2 -2
- package/guides/square-api.md +107 -0
- package/package.json +1 -1
- package/plugins/claude/.claude-plugin/plugin.json +1 -1
- package/plugins/claude/skills/square/SKILL.md +3 -3
- package/plugins/codex/.codex-plugin/plugin.json +1 -1
- package/plugins/opencode/index.ts +1 -1
- package/plugins/paseo/client/markdown.tsx +16 -6
- package/plugins/pi/square-pi.js +70 -30
- package/skills/square/SKILL.md +3 -3
- package/dist/square-application.d.ts +0 -38
- package/dist/square-application.js +0 -683
- package/guides/application-boundary.md +0 -32
- package/guides/application-facade-design.md +0 -98
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import crypto from 'node:crypto';
|
|
3
|
+
import { setTimeout as sleep } from 'node:timers/promises';
|
|
4
|
+
import { SquareError, isSquareError, nameKey, validateName } from './model.js';
|
|
5
|
+
import { resolveKnownName, validateDoneBody } from './decisions.js';
|
|
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, 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, 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 } from './notifications.js';
|
|
17
|
+
import { acquireWatchLease, ownsWatchLease, pulseWatchLease, releaseWatchLease } from './wakes.js';
|
|
18
|
+
import { createSquare, diagnoseSquareFile } from './artifact.js';
|
|
19
|
+
import { migrateSquareFile } from './square-migration.js';
|
|
20
|
+
import { discoverSquares } from './list.js';
|
|
21
|
+
import { streamSquare } from './stream.js';
|
|
22
|
+
export { createSquareResourceOwner } from './open-square.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';
|
|
28
|
+
export function squareLocation(input) {
|
|
29
|
+
const selected = input.path ?? (input.env ?? process.env).SQUARE_LOCATION;
|
|
30
|
+
if (selected === undefined || selected.trim() === '')
|
|
31
|
+
throw new SquareError('invalid_args', 'Square needs a location');
|
|
32
|
+
return path.resolve(input.cwd ?? process.cwd(), selected);
|
|
33
|
+
}
|
|
34
|
+
function checkControl(control) {
|
|
35
|
+
if (control?.signal?.aborted)
|
|
36
|
+
throw control.signal.reason ?? new Error('Square operation was cancelled');
|
|
37
|
+
}
|
|
38
|
+
export function squareClock(env, clock) {
|
|
39
|
+
if (clock !== undefined)
|
|
40
|
+
return clock;
|
|
41
|
+
if (env.SQUARE_NOW_MS === undefined)
|
|
42
|
+
return Date.now;
|
|
43
|
+
return () => {
|
|
44
|
+
const value = Number.parseInt(env.SQUARE_NOW_MS, 10);
|
|
45
|
+
if (!Number.isFinite(value))
|
|
46
|
+
throw new SquareError('invalid_args', 'Invalid SQUARE_NOW_MS: expected integer milliseconds.');
|
|
47
|
+
return value;
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function failure(error, detail) {
|
|
51
|
+
const normalized = error instanceof Error ? error : new Error(String(error), { cause: error });
|
|
52
|
+
Object.assign(normalized, { operation: detail });
|
|
53
|
+
return normalized;
|
|
54
|
+
}
|
|
55
|
+
function defaultTransport(square) {
|
|
56
|
+
let transport;
|
|
57
|
+
return () => transport ??= square.wakeTransport !== undefined ? Promise.resolve(square.wakeTransport)
|
|
58
|
+
: square.location === 'memory' || square.hostLedger === undefined ? Promise.resolve(undefined)
|
|
59
|
+
: createDefaultWakeTransport(square.hostLedger, square.clock, square.env).catch(() => undefined);
|
|
60
|
+
}
|
|
61
|
+
function observedContext(square, standingCount, held, participant, now = square.clock()) {
|
|
62
|
+
return { location: square.location, standingCount, held, now,
|
|
63
|
+
...(participant === undefined ? {} : { participant }), automaticDeliveryAvailable: hasAutomaticDeliveryIdentity(square.env) };
|
|
64
|
+
}
|
|
65
|
+
async function contextObservation(square, participant, control) {
|
|
66
|
+
const observation = (await square.artifact.read((facts) => ({ standingCount: inSquareCount(facts), held: facts.hold().active }), control?.signal)).value;
|
|
67
|
+
return observedContext(square, observation.standingCount, observation.held, participant);
|
|
68
|
+
}
|
|
69
|
+
async function prepareExpression(square, requested, control) {
|
|
70
|
+
try {
|
|
71
|
+
checkControl(control);
|
|
72
|
+
const result = await join(square, requested, control, { requireKnown: true });
|
|
73
|
+
return { participant: result.name, activity: result.activity };
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
throw failure(error, { operation: 'express', subject: 'caller', participant: requested, expressEffect: 'unsent' });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/** One retry/commit owner for every participant, regardless of transport. */
|
|
80
|
+
async function landExpression(square, name, body, options, transport, control) {
|
|
81
|
+
let effect = 'unsent';
|
|
82
|
+
let waited = false, heldWaitedMs = 0;
|
|
83
|
+
try {
|
|
84
|
+
const wakeTransport = await transport();
|
|
85
|
+
while (true) {
|
|
86
|
+
checkControl(control);
|
|
87
|
+
try {
|
|
88
|
+
effect = 'unknown';
|
|
89
|
+
return { ...await express({ ...square, wakeTransport }, name, body, options, control), waited };
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
if (isSquareError(error) && error.facts?.activityUnsent === true)
|
|
93
|
+
effect = 'unsent';
|
|
94
|
+
if (!isSquareError(error) || options.noWait || (error.code !== 'held' && error.code !== 'throttled'))
|
|
95
|
+
throw error;
|
|
96
|
+
if (error.code === 'held' && heldWaitedMs >= HELD_WAIT_BUDGET_MS)
|
|
97
|
+
throw error;
|
|
98
|
+
waited = true;
|
|
99
|
+
control?.onProgress?.({ kind: 'waiting', reason: error.code,
|
|
100
|
+
...(error.code === 'throttled' ? { delayMs: error.facts?.retryAfterMs ?? 1 } : {}),
|
|
101
|
+
...(error.facts?.holder === undefined ? {} : { holder: error.facts.holder }),
|
|
102
|
+
...(error.facts?.holdReason === undefined ? {} : { holdReason: error.facts.holdReason }),
|
|
103
|
+
});
|
|
104
|
+
if (error.code === 'throttled')
|
|
105
|
+
await sleep(error.facts?.retryAfterMs ?? 1, undefined, { signal: control?.signal });
|
|
106
|
+
else {
|
|
107
|
+
const started = Date.now();
|
|
108
|
+
const observation = await square.artifact.read((facts) => facts.hold().active, control?.signal);
|
|
109
|
+
if (observation.value)
|
|
110
|
+
await square.artifact.changed(observation.version, Math.max(1, HELD_WAIT_BUDGET_MS - heldWaitedMs), control?.signal);
|
|
111
|
+
heldWaitedMs += Date.now() - started;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
const recipient = isSquareError(error) && ['not_standing', 'bell_mention_conflict'].includes(error.facts?.reason ?? '');
|
|
118
|
+
throw failure(error, { operation: 'express', participant: name, subject: recipient ? 'recipient' : 'caller', expressEffect: effect });
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/** Leases, quiet waiting and receipt selection share one lifecycle. */
|
|
122
|
+
async function catchOwned(square, requested, options = {}, control) {
|
|
123
|
+
let name = requested, subject = 'caller';
|
|
124
|
+
let leaseId;
|
|
125
|
+
let waitStarted = false, replaced = false;
|
|
126
|
+
let selection = options;
|
|
127
|
+
const idle = options.idle ?? 0;
|
|
128
|
+
async function complete(caught, stop) {
|
|
129
|
+
const selected = caught ?? (await square.artifact.read((facts) => observeCatchSelection(facts, name, [], {
|
|
130
|
+
unseenOnly: true, senders: selection.from, ...(selection.mention === true ? { attentionOnly: true } : {}),
|
|
131
|
+
}, square.clock()))).value;
|
|
132
|
+
const terminal = stop === 'empty-now' || stop === undefined ? selected.terminalStatus ?? stop : stop;
|
|
133
|
+
if (leaseId !== undefined) {
|
|
134
|
+
await releaseWatchLease(square, name, leaseId).catch(() => undefined);
|
|
135
|
+
leaseId = undefined;
|
|
136
|
+
}
|
|
137
|
+
return { selection: selected, stop: terminal, waitStarted, replaced };
|
|
138
|
+
}
|
|
139
|
+
const empty = (stop) => complete(undefined, stop);
|
|
140
|
+
try {
|
|
141
|
+
checkControl(control);
|
|
142
|
+
if (!Number.isFinite(idle) || idle < 0)
|
|
143
|
+
throw new SquareError('invalid_args', 'Catch idle duration must be a non-negative number');
|
|
144
|
+
if (options.id !== undefined && ['idle', 'from', 'mention', 'limit', 'replace'].some((key) => Object.hasOwn(options, key)))
|
|
145
|
+
throw new SquareError('invalid_args', 'Catch id cannot be combined with idle, from, mention, limit or replace');
|
|
146
|
+
if (Object.hasOwn(options, 'replace') && idle === 0)
|
|
147
|
+
throw new SquareError('invalid_args', 'Catch replacement requires a positive idle duration');
|
|
148
|
+
catchLimit(options.limit);
|
|
149
|
+
const from = (await square.artifact.read((facts) => {
|
|
150
|
+
name = resolveKnownName(facts, requested);
|
|
151
|
+
subject = 'filter';
|
|
152
|
+
return options.from?.map((target) => resolveKnownName(facts, target));
|
|
153
|
+
}, control?.signal)).value;
|
|
154
|
+
selection = { ...options, ...(from === undefined ? {} : { from }) };
|
|
155
|
+
subject = 'caller';
|
|
156
|
+
if (idle === 0) {
|
|
157
|
+
const data = await catchUp(square, name, selection, control);
|
|
158
|
+
return await complete(data, data.activities.length === 0 ? 'empty-now' : undefined);
|
|
159
|
+
}
|
|
160
|
+
if (!await assertLiveOwner(square, name))
|
|
161
|
+
throw new SquareError('already_joined', `${name} already stands here — another session holds the name`);
|
|
162
|
+
const watch = {
|
|
163
|
+
...(from === undefined ? {} : { participants: [...from] }), ...(options.mention ? { mention: name } : {}),
|
|
164
|
+
...(options.replace === undefined ? {} : { replace: options.replace }),
|
|
165
|
+
};
|
|
166
|
+
const start = await acquireWatchLease(square, name, crypto.randomUUID(), watch, control?.signal);
|
|
167
|
+
if (start.type === 'active')
|
|
168
|
+
return await empty('already-active');
|
|
169
|
+
leaseId = start.leaseId;
|
|
170
|
+
waitStarted = true;
|
|
171
|
+
replaced = start.replaced;
|
|
172
|
+
control?.onProgress?.({ kind: 'catch-started', participant: name, idleMs: idle, replacementRequested: options.replace ?? false, replaced });
|
|
173
|
+
let quietSince = square.clock(), wasHeld = false, nextHeartbeat = start.heartbeatAt + WATCH_HEARTBEAT_MS;
|
|
174
|
+
while (true) {
|
|
175
|
+
checkControl(control);
|
|
176
|
+
// Capture before evaluation so a commit during selection cannot be missed.
|
|
177
|
+
const baseline = await square.artifact.revision(control?.signal);
|
|
178
|
+
const pulse = await pulseWatchLease(square, name, leaseId, watch, square.clock() >= nextHeartbeat, control?.signal);
|
|
179
|
+
if (pulse.type === 'replaced')
|
|
180
|
+
return await empty('replaced');
|
|
181
|
+
if (pulse.type === 'terminal')
|
|
182
|
+
return await empty(pulse.status);
|
|
183
|
+
if (pulse.heartbeatAt !== undefined)
|
|
184
|
+
nextHeartbeat = pulse.heartbeatAt + WATCH_HEARTBEAT_MS;
|
|
185
|
+
if (pulse.type === 'sleep') {
|
|
186
|
+
const caught = await catchUp(square, name, selection, control, leaseId);
|
|
187
|
+
if (caught === null)
|
|
188
|
+
return await empty('replaced');
|
|
189
|
+
if (caught.activities.length > 0)
|
|
190
|
+
return await complete(caught);
|
|
191
|
+
}
|
|
192
|
+
if (pulse.type === 'held' || wasHeld)
|
|
193
|
+
quietSince = square.clock();
|
|
194
|
+
wasHeld = pulse.type === 'held';
|
|
195
|
+
if (!wasHeld && square.clock() - quietSince >= idle) {
|
|
196
|
+
const owns = await ownsWatchLease(square, name, leaseId, control?.signal);
|
|
197
|
+
return await empty(owns ? 'stale' : 'replaced');
|
|
198
|
+
}
|
|
199
|
+
const deadline = Math.min(nextHeartbeat, wasHeld ? Infinity : quietSince + idle);
|
|
200
|
+
await square.artifact.changed(baseline, Math.max(0, deadline - square.clock()), control?.signal);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
catch (error) {
|
|
204
|
+
throw failure(error, { operation: 'catch', subject, participant: name });
|
|
205
|
+
}
|
|
206
|
+
finally {
|
|
207
|
+
// An aborted request still releases its own lease, never its replacement's.
|
|
208
|
+
if (leaseId !== undefined)
|
|
209
|
+
await releaseWatchLease(square, name, leaseId).catch(() => undefined);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
class ParticipantHandle {
|
|
213
|
+
name;
|
|
214
|
+
owner;
|
|
215
|
+
square;
|
|
216
|
+
transport;
|
|
217
|
+
constructor(name, owner, square, transport) {
|
|
218
|
+
this.name = name;
|
|
219
|
+
this.owner = owner;
|
|
220
|
+
this.square = square;
|
|
221
|
+
this.transport = transport;
|
|
222
|
+
}
|
|
223
|
+
async express(body, options = {}, control) {
|
|
224
|
+
const entry = await prepareExpression(this.square, this.name, control);
|
|
225
|
+
const name = entry.participant;
|
|
226
|
+
let result;
|
|
227
|
+
try {
|
|
228
|
+
const feed = !options.observe ? undefined : (await this.square.artifact.read((facts) => {
|
|
229
|
+
const pendingPublic = directedSuffix(facts, name);
|
|
230
|
+
return { pendingPublic, sayNumbers: facts.sayOrdinals(pendingPublic.map((item) => item.index)),
|
|
231
|
+
perceptions: Object.fromEntries(pendingPublic.map((item) => [item.index, 'full'])) };
|
|
232
|
+
}, control?.signal)).value;
|
|
233
|
+
result = await landExpression(this.square, name, body, options, this.transport, control);
|
|
234
|
+
const { observation: committed, ...data } = result;
|
|
235
|
+
return { ...data, ...(feed === undefined ? {} : { observation: {
|
|
236
|
+
...observedContext(this.square, committed.standingCount, committed.held, name, committed.now), ...committed, ...feed, participant: name,
|
|
237
|
+
...(entry.activity === null ? {} : { reentry: entry.activity }),
|
|
238
|
+
} }) };
|
|
239
|
+
}
|
|
240
|
+
catch (error) {
|
|
241
|
+
if (result === undefined && error?.operation?.operation === 'express')
|
|
242
|
+
throw error;
|
|
243
|
+
throw failure(error, { operation: 'express', participant: name, subject: 'caller', expressEffect: result === undefined ? 'unsent' : 'landed',
|
|
244
|
+
...(result === undefined ? {} : { landedId: result.activity.id }) });
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
async listener(verb, target, options, control) {
|
|
248
|
+
const name = (await resolveParticipant(this.square, this.name, control)).name;
|
|
249
|
+
const { observation: committed, ...result } = await (verb === 'listen' ? listen : ignore)(this.square, name, target, control);
|
|
250
|
+
return { ...result, ...(!options.observe ? {} : { observation: {
|
|
251
|
+
...observedContext(this.square, committed.standingCount, committed.held, name, committed.now),
|
|
252
|
+
participant: name, requestedTarget: target, target: result.activity?.target ?? target,
|
|
253
|
+
} }) };
|
|
254
|
+
}
|
|
255
|
+
listen(target, options = {}, control) { return this.listener('listen', target, options, control); }
|
|
256
|
+
ignore(target, options = {}, control) { return this.listener('ignore', target, options, control); }
|
|
257
|
+
async listening(options = {}, control) {
|
|
258
|
+
const name = (await resolveParticipant(this.square, this.name, control)).name;
|
|
259
|
+
return { listening: await listening(this.square, name, control),
|
|
260
|
+
...(!options.observe ? {} : { observation: await contextObservation(this.square, name, control) }) };
|
|
261
|
+
}
|
|
262
|
+
async catch(options = {}, control) {
|
|
263
|
+
const result = await catchOwned(this.square, this.name, options, control), view = result.selection, stop = result.stop;
|
|
264
|
+
const presence = !options.observe ? undefined : await catchPresence(this.square, control).catch(() => undefined);
|
|
265
|
+
const status = stop === 'already-active' || stop === 'replaced' || stop === 'capped' || stop === 'quorum' ? stop : undefined;
|
|
266
|
+
return { activities: view.activities.map((activity) => ({ ...toPublicActivity(activity), perception: 'full' })),
|
|
267
|
+
consumedThrough: view.consumedThrough, remaining: view.remaining, idleExpired: stop === 'stale',
|
|
268
|
+
...(status === undefined ? {} : { status }), ...(!options.observe ? {} : { observation: {
|
|
269
|
+
...observedContext(this.square, view.standingCount, view.held, view.participant, view.now), participant: view.participant,
|
|
270
|
+
sayNumbers: view.sayNumbers, ownActivityCount: view.ownActivityCount, hardCap: view.hardCap, stop,
|
|
271
|
+
...(presence === undefined ? { enrichmentUnavailable: true } : { presence }), waitStarted: result.waitStarted, replaced: result.replaced,
|
|
272
|
+
} }) };
|
|
273
|
+
}
|
|
274
|
+
history(query, control) { return this.owner.history(query, control); }
|
|
275
|
+
async lifecycle(verb, body, options, control) {
|
|
276
|
+
if (verb === 'done')
|
|
277
|
+
validateDoneBody(body);
|
|
278
|
+
const identity = localSessionIdentities(this.square.env ?? {})[0];
|
|
279
|
+
const owner = verb !== 'done' || identity === undefined || this.square.hostLedger === undefined ? undefined
|
|
280
|
+
: await readParticipantOwner(this.square.location, this.name, this.square.hostLedger);
|
|
281
|
+
const fenced = identity !== undefined && owner?.session === identity.sessionId && presenceEpoch(owner) > 0;
|
|
282
|
+
const name = fenced ? this.name : (await join(this.square, this.name, control)).name;
|
|
283
|
+
const result = verb === 'done' ? await done(this.square, name, body ?? '', fenced ? { expectedEpoch: presenceEpoch(owner) } : {}, control)
|
|
284
|
+
: verb === 'hold' ? await hold(this.square, name, body, control) : await resume(this.square, name, control);
|
|
285
|
+
const { observation: committed, ...receipt } = result;
|
|
286
|
+
return { ...receipt, ...(!options.observe ? {} : { observation: {
|
|
287
|
+
...observedContext(this.square, committed.standingCount, committed.held, name, committed.now), ...committed,
|
|
288
|
+
} }) };
|
|
289
|
+
}
|
|
290
|
+
hold(reason, options = {}, control) { return this.lifecycle('hold', reason, options, control); }
|
|
291
|
+
resume(options = {}, control) { return this.lifecycle('resume', undefined, options, control); }
|
|
292
|
+
done(body, options = {}, control) { return this.lifecycle('done', body, options, control); }
|
|
293
|
+
}
|
|
294
|
+
/** One concrete square resource. Request handles may borrow a server-owned connection. */
|
|
295
|
+
export class Square {
|
|
296
|
+
location;
|
|
297
|
+
square;
|
|
298
|
+
defaultParticipant;
|
|
299
|
+
transport;
|
|
300
|
+
constructor(location, square, defaultParticipant) {
|
|
301
|
+
this.location = location;
|
|
302
|
+
this.square = square;
|
|
303
|
+
this.defaultParticipant = defaultParticipant;
|
|
304
|
+
this.transport = defaultTransport(square);
|
|
305
|
+
}
|
|
306
|
+
static async at(input, control) {
|
|
307
|
+
const env = { ...(input.env ?? process.env) }, location = squareLocation({ ...input, env });
|
|
308
|
+
return new Square(location, await openSquare(location, { ...input, env, clock: squareClock(env, input.clock), artifact: input.resources?.artifact(location) }, control), input.participant);
|
|
309
|
+
}
|
|
310
|
+
static async build(input, control) {
|
|
311
|
+
checkControl(control);
|
|
312
|
+
const env = { ...(input.env ?? process.env) }, location = squareLocation({ ...input, env });
|
|
313
|
+
await createSquare(location, { hardCap: input.hardCap, throttlePerMinute: input.throttlePerMinute,
|
|
314
|
+
template: input.template, force: input.force ?? false }, input.markdown, control?.signal);
|
|
315
|
+
// A completed build survives cancellation arriving after its commit.
|
|
316
|
+
return Square.at({ ...input, path: location, env });
|
|
317
|
+
}
|
|
318
|
+
static inMemory(input) {
|
|
319
|
+
const env = { ...(input.env ?? process.env) };
|
|
320
|
+
return new Square('memory', buildMemorySquare({ ...input, env, clock: squareClock(env, input.clock) }), input.participant);
|
|
321
|
+
}
|
|
322
|
+
handle(name) { return new ParticipantHandle(name, this, this.square, this.transport); }
|
|
323
|
+
async participant(requested) {
|
|
324
|
+
const selected = requested ?? this.defaultParticipant ?? this.square.env?.SQUARE_PARTICIPANT_NAME;
|
|
325
|
+
if (requested !== undefined || this.defaultParticipant !== undefined || selected?.trim()) {
|
|
326
|
+
validateName(selected);
|
|
327
|
+
return this.handle(selected);
|
|
328
|
+
}
|
|
329
|
+
const discovered = this.square.hostLedger === undefined ? undefined : await localParticipantName(this.location, this.square.hostLedger, this.square.env);
|
|
330
|
+
if (discovered !== undefined)
|
|
331
|
+
return this.handle(discovered);
|
|
332
|
+
throw new SquareError('invalid_args', 'Square needs one unambiguous participant name');
|
|
333
|
+
}
|
|
334
|
+
async join(requested, options = {}, control) {
|
|
335
|
+
let name = requested;
|
|
336
|
+
try {
|
|
337
|
+
checkControl(control);
|
|
338
|
+
name = (await this.participant(requested)).name;
|
|
339
|
+
const last = options.last ?? HISTORY_DEFAULT_LIMIT;
|
|
340
|
+
if (!Number.isSafeInteger(last) || last < 1 || last > HISTORY_MAX_LIMIT)
|
|
341
|
+
throw new SquareError('invalid_args', 'Join history limit must be between 1 and 100');
|
|
342
|
+
let committed;
|
|
343
|
+
let result;
|
|
344
|
+
if (options.takeover && await currentParticipant(this.square, name, control) !== undefined) {
|
|
345
|
+
const taken = await takeover(this.square, name, control);
|
|
346
|
+
result = { participant: taken.name, activity: taken.activities.at(-1) ?? null, kind: 'taken-over', participantCount: taken.participantCount };
|
|
347
|
+
committed = taken.observation;
|
|
348
|
+
}
|
|
349
|
+
else {
|
|
350
|
+
const joined = await join(this.square, name, control);
|
|
351
|
+
result = { participant: joined.name, activity: joined.activity, kind: joined.reconnected ? 'reconnected' : 'joined', participantCount: joined.participantCount };
|
|
352
|
+
committed = joined.observation;
|
|
353
|
+
}
|
|
354
|
+
const entry = !options.observe ? undefined : await entryPresentation(this.square, result.participant, last, control).catch(() => undefined);
|
|
355
|
+
return { ...result, participant: this.handle(result.participant), ...(!options.observe ? {} : {
|
|
356
|
+
observation: {
|
|
357
|
+
...observedContext(this.square, committed.standingCount, committed.held, result.participant, committed.now),
|
|
358
|
+
...(entry === undefined ? { enrichmentUnavailable: true } : { entry }),
|
|
359
|
+
},
|
|
360
|
+
}) };
|
|
361
|
+
}
|
|
362
|
+
catch (error) {
|
|
363
|
+
throw failure(error, { operation: 'join', subject: 'caller', participant: name });
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
doneOwnedSession(name, body, expectedEpoch, control) { return done(this.square, name, body, { expectedEpoch }, control); }
|
|
367
|
+
async implicitJoin(name, control) {
|
|
368
|
+
const result = await implicitJoin(this.square, name, control);
|
|
369
|
+
return { state: result.state, ...(result.state === 'done' ? {} : { participant: this.handle(result.name) }) };
|
|
370
|
+
}
|
|
371
|
+
snapshot(control) { return snapshot(this.square, control); }
|
|
372
|
+
async history(query = {}, control) {
|
|
373
|
+
if (query.limit !== undefined && query.limit !== null && (!Number.isSafeInteger(query.limit) || query.limit < 1 || query.limit > HISTORY_MAX_LIMIT))
|
|
374
|
+
throw new SquareError('invalid_args', `History limit must be a positive integer no greater than ${HISTORY_MAX_LIMIT}`);
|
|
375
|
+
if (query.grep !== undefined && query.fixed !== undefined)
|
|
376
|
+
throw new SquareError('invalid_args', 'History cannot combine regular-expression and literal search');
|
|
377
|
+
if (query.grep === '' || query.fixed === '')
|
|
378
|
+
throw new SquareError('invalid_args', 'History search requires non-empty text');
|
|
379
|
+
if (query.since !== undefined && !Number.isFinite(query.since))
|
|
380
|
+
throw new SquareError('invalid_args', 'History timestamp must be finite');
|
|
381
|
+
for (const count of [query.contextBefore, query.contextAfter])
|
|
382
|
+
if (count !== undefined && (!Number.isSafeInteger(count) || count < 0))
|
|
383
|
+
throw new SquareError('invalid_args', 'History context must be a non-negative safe integer');
|
|
384
|
+
const limit = query.limit === undefined ? (query.at?.length ? null : HISTORY_DEFAULT_LIMIT) : query.limit;
|
|
385
|
+
const contextual = !!query.at?.length || query.contextBefore !== undefined || query.contextAfter !== undefined;
|
|
386
|
+
const kinds = contextual || query.since !== undefined ? undefined : NON_READ;
|
|
387
|
+
return (await this.square.artifact.read((facts) => {
|
|
388
|
+
const selected = selectHistory(facts, query, limit, kinds, query.observe === true);
|
|
389
|
+
if (contextual && limit === null && selected.totalMatches > HISTORY_MAX_LIMIT)
|
|
390
|
+
throw failure(new SquareError('invalid_args', `History context exceeds ${HISTORY_MAX_LIMIT} activities`), { operation: 'history', historyBounds: { maxLimit: HISTORY_MAX_LIMIT, totalMatches: selected.totalMatches } });
|
|
391
|
+
const activities = publicActivities(selected.activities);
|
|
392
|
+
if (!query.observe)
|
|
393
|
+
return { activities };
|
|
394
|
+
const indexes = selected.activities.map((activity) => activity.index), presenceAnchors = {};
|
|
395
|
+
for (const participant of facts.roster()) {
|
|
396
|
+
const anchor = presenceAnchor(facts, participant.name);
|
|
397
|
+
if (indexes.includes(anchor))
|
|
398
|
+
(presenceAnchors[anchor] ??= []).push(participant.name);
|
|
399
|
+
}
|
|
400
|
+
return { activities, observation: { ...observedContext(this.square, inSquareCount(facts), facts.hold().active), ...selected,
|
|
401
|
+
sayNumbers: facts.sayOrdinals(indexes), presenceAnchors } };
|
|
402
|
+
}, control?.signal)).value;
|
|
403
|
+
}
|
|
404
|
+
async status(options = {}, control) {
|
|
405
|
+
const viewer = !options.observe ? '' : options.viewer ?? (await this.participant().catch(() => undefined))?.name ?? '';
|
|
406
|
+
return (await this.square.artifact.read((facts) => queryStatus(facts, {
|
|
407
|
+
location: this.location, now: this.square.clock(), automaticDeliveryAvailable: hasAutomaticDeliveryIdentity(this.square.env),
|
|
408
|
+
}, viewer, options.observe === true), control?.signal)).value;
|
|
409
|
+
}
|
|
410
|
+
async participants(options = {}, control) {
|
|
411
|
+
return (await this.square.artifact.read((facts) => queryParticipants(facts, {
|
|
412
|
+
location: this.location, now: this.square.clock(), automaticDeliveryAvailable: hasAutomaticDeliveryIdentity(this.square.env),
|
|
413
|
+
}, options.observe === true), control?.signal)).value;
|
|
414
|
+
}
|
|
415
|
+
async recovery(requested, control) {
|
|
416
|
+
const name = (await this.participant(requested).catch(() => undefined))?.name;
|
|
417
|
+
const facts = name === undefined ? undefined : await recoveryObservation(this.square, name, control).catch(() => undefined);
|
|
418
|
+
return facts ?? await contextObservation(this.square, name, control);
|
|
419
|
+
}
|
|
420
|
+
stream(options, control) { return streamSquare(this.location, options, control, this.square); }
|
|
421
|
+
async recognize(env) {
|
|
422
|
+
if (this.square.hostLedger === undefined)
|
|
423
|
+
return null;
|
|
424
|
+
const candidates = (await Promise.all(sessionIdsFromEnvironment(env).map((sessionId) => projectSessionBindings({ hostLedger: this.square.hostLedger, location: this.location, sessionId })))).flat();
|
|
425
|
+
if (candidates.length !== 1)
|
|
426
|
+
return null;
|
|
427
|
+
const name = await currentParticipant(this.square, candidates[0].participant);
|
|
428
|
+
return name === undefined ? null : this.handle(name);
|
|
429
|
+
}
|
|
430
|
+
close() { return closeOpenSquare(this.square); }
|
|
431
|
+
endOwnedSession(name, sessionId, expectedEpoch) { return endOwnedSession(this.square, name, sessionId, expectedEpoch); }
|
|
432
|
+
retireEndedSessionRoutes(sessionId) { return retireEndedSessionRoutes(this.square, sessionId); }
|
|
433
|
+
}
|
|
434
|
+
export function diagnoseSquare(input, control) { checkControl(control); return diagnoseSquareFile(squareLocation(input)); }
|
|
435
|
+
export function migrateSquare(input, control) { checkControl(control); return migrateSquareFile(squareLocation(input), control?.signal); }
|
|
436
|
+
export function discover(input, options = {}, control) {
|
|
437
|
+
const env = { ...(input.env ?? process.env) };
|
|
438
|
+
return discoverSquares(input.cwd ?? process.cwd(), options, control, { ...input, env, clock: squareClock(env, input.clock) });
|
|
439
|
+
}
|
|
440
|
+
export async function inbox(input, sessionId, options = {}, control) {
|
|
441
|
+
checkControl(control);
|
|
442
|
+
const env = { ...(input.env ?? process.env) }, hostLedger = input.hostLedger ?? hostLedgerForEnv(env), limit = options.limit ?? 20;
|
|
443
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100)
|
|
444
|
+
throw new SquareError('invalid_args', 'Inbox limit must be between 1 and 100');
|
|
445
|
+
const rows = [];
|
|
446
|
+
for (const binding of await projectSessionBindings({ hostLedger, sessionId })) {
|
|
447
|
+
checkControl(control);
|
|
448
|
+
let square;
|
|
449
|
+
try {
|
|
450
|
+
square = await openSquare(binding.location, { ...input, env, hostLedger, clock: squareClock(env, input.clock) }, control);
|
|
451
|
+
const row = (await square.artifact.read((facts) => {
|
|
452
|
+
const participant = facts.participant(binding.participant);
|
|
453
|
+
return !participant?.joined ? undefined : { name: participant.name, squarePath: binding.location,
|
|
454
|
+
pending: facts.directedCount(participant.name, { unseenOnly: true }) };
|
|
455
|
+
}, control?.signal)).value;
|
|
456
|
+
if (row !== undefined)
|
|
457
|
+
rows.push(row);
|
|
458
|
+
}
|
|
459
|
+
catch (error) {
|
|
460
|
+
if (control?.signal?.aborted)
|
|
461
|
+
throw error;
|
|
462
|
+
}
|
|
463
|
+
finally {
|
|
464
|
+
if (square !== undefined)
|
|
465
|
+
await closeOpenSquare(square);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
const compare = (left, right) => left < right ? -1 : left > right ? 1 : 0;
|
|
469
|
+
rows.sort((a, b) => compare(a.squarePath, b.squarePath) || compare(nameKey(a.name), nameKey(b.name)) || compare(a.name, b.name));
|
|
470
|
+
return { rows: rows.slice(0, limit), total: rows.length };
|
|
471
|
+
}
|