@astrosheep/square 0.3.85 → 0.3.87
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/artifact.d.ts +6 -1
- package/dist/artifact.js +79 -10
- package/dist/boundary-presentation.d.ts +5 -4
- package/dist/boundary-presentation.js +21 -25
- package/dist/catch-decisions.js +2 -1
- package/dist/claude-delivery-status.d.ts +8 -2
- package/dist/claude-delivery-status.js +26 -10
- package/dist/claude-delivery.d.ts +8 -4
- package/dist/claude-delivery.js +114 -104
- package/dist/claude-mcp-context.js +2 -2
- package/dist/claude-mod.js +119 -105
- package/dist/cli/context.d.ts +3 -1
- package/dist/cli/context.js +25 -1
- package/dist/cli/express-presentation.d.ts +3 -1
- package/dist/cli/express-presentation.js +15 -12
- package/dist/cli/mcp-server-command.js +2 -2
- package/dist/cli/observation-commands.js +2 -1
- package/dist/cli/registry.js +0 -2
- package/dist/cli/square-commands.d.ts +4 -2
- package/dist/cli/square-commands.js +21 -6
- package/dist/codex-hook.d.ts +3 -3
- package/dist/codex-hook.js +4 -4
- package/dist/codex-queue.d.ts +5 -6
- package/dist/codex-queue.js +9 -5
- package/dist/decisions.d.ts +3 -2
- package/dist/decisions.js +18 -6
- package/dist/delivery-health.d.ts +32 -2
- package/dist/delivery-health.js +108 -45
- package/dist/delivery-operations.d.ts +1 -2
- package/dist/delivery-operations.js +155 -196
- package/dist/delivery.d.ts +14 -23
- package/dist/delivery.js +48 -18
- package/dist/file-lock.d.ts +8 -1
- package/dist/file-lock.js +2 -0
- package/dist/help.js +12 -14
- package/dist/hints.js +2 -3
- package/dist/inbox.d.ts +35 -9
- package/dist/inbox.js +228 -144
- package/dist/index.d.ts +4 -4
- package/dist/index.js +1 -3
- package/dist/list.js +9 -4
- package/dist/mcp.js +12 -1
- package/dist/model.d.ts +39 -5
- package/dist/model.js +84 -0
- package/dist/notifications.d.ts +8 -6
- package/dist/notifications.js +56 -143
- package/dist/operation-presentation.js +12 -14
- package/dist/participant-identity.d.ts +0 -3
- package/dist/participant-identity.js +0 -31
- package/dist/paseo-delivery.d.ts +5 -6
- package/dist/paseo-delivery.js +13 -8
- package/dist/paseo.d.ts +1 -1
- package/dist/paseo.js +1 -1
- package/dist/plugins/opencode/delivery.d.ts +8 -3
- package/dist/plugins/opencode/delivery.js +187 -132
- package/dist/plugins/opencode/index.js +12 -38
- package/dist/ports.d.ts +112 -40
- package/dist/presentation-operations.d.ts +1 -1
- package/dist/presentation-operations.js +51 -35
- package/dist/presentation.d.ts +0 -1
- package/dist/presentation.js +18 -13
- package/dist/seats.d.ts +7 -0
- package/dist/seats.js +27 -0
- package/dist/session-lifecycle.d.ts +2 -2
- package/dist/session-lifecycle.js +10 -12
- package/dist/sqlite-square.d.ts +53 -0
- package/dist/sqlite-square.js +602 -0
- package/dist/square-api.d.ts +12 -41
- package/dist/square-api.js +26 -474
- package/dist/square-backend.d.ts +13 -0
- package/dist/square-core.d.ts +3 -2
- package/dist/square-domain.d.ts +12 -5
- package/dist/square-domain.js +163 -7
- package/dist/square-facade.d.ts +16 -10
- package/dist/square-index.d.ts +24 -0
- package/dist/square-index.js +131 -0
- package/dist/square-legacy-framing.d.ts +9 -0
- package/dist/square-legacy-framing.js +56 -0
- package/dist/square-location.d.ts +15 -0
- package/dist/square-location.js +76 -0
- package/dist/square-migration-host-ledger.d.ts +26 -0
- package/dist/square-migration-host-ledger.js +168 -0
- package/dist/square-migration.d.ts +5 -5
- package/dist/square-migration.js +373 -71
- package/dist/square-sqlite.d.ts +9 -9
- package/dist/square-sqlite.js +329 -79
- package/dist/square-stdio.d.ts +22 -0
- package/dist/square-stdio.js +222 -0
- package/dist/square-tools.js +30 -3
- package/dist/stdio-server.d.ts +3 -0
- package/dist/stdio-server.js +192 -0
- package/dist/stream.d.ts +3 -2
- package/dist/stream.js +9 -10
- package/dist/views.d.ts +9 -14
- package/dist/views.js +50 -70
- package/dist/wakes.d.ts +6 -5
- package/dist/wakes.js +20 -17
- package/guides/application-facade-delivery-evidence.md +1 -1
- package/guides/backends.md +180 -0
- package/guides/claude-native.md +9 -9
- package/guides/mcp.md +1 -1
- package/guides/migration.md +107 -67
- package/guides/row-storage-design.md +1 -1
- package/guides/square-api.md +69 -30
- package/package.json +1 -1
- package/plugins/claude/.claude-plugin/plugin.json +1 -1
- package/plugins/claude/hooks/register.js +1 -1
- package/plugins/claude/skills/square/SKILL.md +7 -7
- package/plugins/claude/types.d.ts +1 -1
- package/plugins/codex/.codex-plugin/plugin.json +1 -1
- package/plugins/opencode/delivery.ts +170 -118
- package/plugins/opencode/index.ts +12 -30
- package/plugins/pi/square-pi.js +58 -20
- package/skills/square/SKILL.md +7 -7
- package/dist/cli/ledger-command.d.ts +0 -12
- package/dist/cli/ledger-command.js +0 -72
- package/dist/host-ledger-file-adapter.d.ts +0 -47
- package/dist/host-ledger-file-adapter.js +0 -384
- package/dist/host-ledger-migration.d.ts +0 -27
- package/dist/host-ledger-migration.js +0 -264
- package/dist/host-ledger-root.d.ts +0 -2
- package/dist/host-ledger-root.js +0 -9
- package/dist/host-ledger-sqlite.d.ts +0 -65
- package/dist/host-ledger-sqlite.js +0 -368
- package/dist/host-ledger.d.ts +0 -195
- package/dist/ledger-maintenance.d.ts +0 -30
- package/dist/ledger-maintenance.js +0 -110
- package/dist/open-square.d.ts +0 -36
- package/dist/open-square.js +0 -81
- package/dist/participant-host.d.ts +0 -22
- package/dist/participant-host.js +0 -92
- package/dist/registry.d.ts +0 -55
- package/dist/registry.js +0 -123
- package/dist/routes.d.ts +0 -39
- package/dist/routes.js +0 -99
- package/dist/square-actions.d.ts +0 -56
- package/dist/square-actions.js +0 -353
- package/dist/square-projections.d.ts +0 -31
- package/dist/square-projections.js +0 -64
- package/dist/wake-attempts.d.ts +0 -10
- package/dist/wake-attempts.js +0 -39
- package/dist/wake-eligibility.d.ts +0 -53
- package/dist/wake-eligibility.js +0 -123
- package/dist/wake-evidence.d.ts +0 -33
- package/dist/wake-evidence.js +0 -32
- package/guides/host-ledger.md +0 -58
- package/guides/indexed-host-ledger-performance.md +0 -39
- /package/dist/{host-ledger.js → square-backend.js} +0 -0
|
@@ -0,0 +1,602 @@
|
|
|
1
|
+
import { formatActivityId } from './square-core.js';
|
|
2
|
+
import { squareLocation, localSquareLocation } from './square-location.js';
|
|
3
|
+
import crypto from 'node:crypto';
|
|
4
|
+
import { setTimeout as sleep } from 'node:timers/promises';
|
|
5
|
+
import { InternalSquareError, SquareError, isSquareError, nameKey, sameBody, validateName } from './model.js';
|
|
6
|
+
import { coreDone, coreHold, coreIgnore, coreListen, coreListening, coreResume, coreUnlisten, decideAct, decideImplicitJoin, decideJoin, resolveKnownName, validateDoneBody } from './decisions.js';
|
|
7
|
+
import { HELD_WAIT_BUDGET_MS, WATCH_HEARTBEAT_MS, inSquareCount, presenceAnchor } from './runtime.js';
|
|
8
|
+
import { catchLimit, decideCatch, observeCatchSelection, selectCatchPage } from './catch-decisions.js';
|
|
9
|
+
import { directedSuffix } from './activity-feed.js';
|
|
10
|
+
import { expressHint } from './hints.js';
|
|
11
|
+
import { asSeatRef, participantAuthority, requireBody, requireJoinName, requireSeat } from './seats.js';
|
|
12
|
+
import { recoveryObservation, entryPresentation, snapshot, queryStatus, queryParticipants, catchPresence, parseRequiredActivityId, toPublicActivity, publicActivities, selectHistory, NON_READ, HISTORY_DEFAULT_LIMIT, HISTORY_MAX_LIMIT } from './views.js';
|
|
13
|
+
import { sessionInboxCounts } from './inbox.js';
|
|
14
|
+
import { rememberSquarePath, squareIndexPath } from './square-index.js';
|
|
15
|
+
import { createDefaultWakeTransport } from './notifications.js';
|
|
16
|
+
import { deliverPending } from './delivery-operations.js';
|
|
17
|
+
import { participantIdentity } from './participant-identity.js';
|
|
18
|
+
import { acquireWatchLease, ownsWatchLease, pulseWatchLease, releaseWatchLease } from './wakes.js';
|
|
19
|
+
import { createArtifactPort, createSquare, diagnoseSquareFile, prepareSquareBuild } from './artifact.js';
|
|
20
|
+
import { migrateSquareFile } from './square-migration.js';
|
|
21
|
+
import { discoverSquares } from './list.js';
|
|
22
|
+
import { streamSquare } from './stream.js';
|
|
23
|
+
export { createSquareResourceOwner } from './artifact.js';
|
|
24
|
+
export { formatActivityId, parseActivityId, MAX_IDENTITY_SET_SIZE } from './square-core.js';
|
|
25
|
+
export { CATCH_DEFAULT_LIMIT, CATCH_MAX_LIMIT } from './catch-decisions.js';
|
|
26
|
+
export { HELD_WAIT_BUDGET_MS } from './runtime.js';
|
|
27
|
+
export { HISTORY_DEFAULT_LIMIT, HISTORY_MAX_LIMIT } from './views.js';
|
|
28
|
+
export { DEFAULT_LIST_DEPTH, MAX_LIST_DEPTH, DEFAULT_LIST_LIMIT, MAX_LIST_LIMIT } from './list.js';
|
|
29
|
+
function checkControl(control) {
|
|
30
|
+
if (control?.signal?.aborted)
|
|
31
|
+
throw control.signal.reason ?? new Error('Square operation was cancelled');
|
|
32
|
+
}
|
|
33
|
+
export function squareClock(env, clock) {
|
|
34
|
+
if (clock !== undefined)
|
|
35
|
+
return clock;
|
|
36
|
+
if (env.SQUARE_NOW_MS === undefined)
|
|
37
|
+
return Date.now;
|
|
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
|
+
function failure(error, detail) {
|
|
44
|
+
const normalized = error instanceof Error ? error : new Error(String(error), { cause: error });
|
|
45
|
+
Object.assign(normalized, { operation: detail });
|
|
46
|
+
return normalized;
|
|
47
|
+
}
|
|
48
|
+
function rejectUnsentActivity(operation) {
|
|
49
|
+
try {
|
|
50
|
+
return operation();
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
if (isSquareError(error))
|
|
54
|
+
error.facts = { ...error.facts, activityUnsent: true };
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/** A generic managed address must not displace the receiver's native registration on reconnect. */
|
|
59
|
+
function canRefreshWakeAddress(current, proposed) {
|
|
60
|
+
return proposed.kind !== 'paseo' || current === undefined || current.kind === 'paseo';
|
|
61
|
+
}
|
|
62
|
+
/** One concrete local resource, including its entry-resolved dependencies and lifetime. */
|
|
63
|
+
export class SqliteSquare {
|
|
64
|
+
location;
|
|
65
|
+
artifact;
|
|
66
|
+
clock;
|
|
67
|
+
body;
|
|
68
|
+
wakeAddress;
|
|
69
|
+
wakeTransport;
|
|
70
|
+
indexPath;
|
|
71
|
+
automaticDelivery;
|
|
72
|
+
ownsArtifact;
|
|
73
|
+
defaultParticipant;
|
|
74
|
+
constructor(location, artifact, clock, body, wakeAddress, wakeTransport, indexPath, automaticDelivery = true, ownsArtifact = true, defaultParticipant) {
|
|
75
|
+
this.location = location;
|
|
76
|
+
this.artifact = artifact;
|
|
77
|
+
this.clock = clock;
|
|
78
|
+
this.body = body;
|
|
79
|
+
this.wakeAddress = wakeAddress;
|
|
80
|
+
this.wakeTransport = wakeTransport;
|
|
81
|
+
this.indexPath = indexPath;
|
|
82
|
+
this.automaticDelivery = automaticDelivery;
|
|
83
|
+
this.ownsArtifact = ownsArtifact;
|
|
84
|
+
this.defaultParticipant = defaultParticipant;
|
|
85
|
+
}
|
|
86
|
+
static async at(input, control, automaticDelivery = true) {
|
|
87
|
+
const env = { ...(input.env ?? process.env) }, location = squareLocation({ ...input, env });
|
|
88
|
+
const clock = squareClock(env, input.clock);
|
|
89
|
+
const wakeTransport = input.wakeTransport ?? (automaticDelivery
|
|
90
|
+
? await createDefaultWakeTransport(env).catch(() => undefined) : undefined);
|
|
91
|
+
// A request can abort its own borrowed reads, never the owner's connection lifetime.
|
|
92
|
+
const borrowed = input.resources?.artifact(location);
|
|
93
|
+
const artifact = borrowed ?? createArtifactPort(location, undefined, control?.signal);
|
|
94
|
+
try {
|
|
95
|
+
await artifact.read(facts => facts.header(), control?.signal);
|
|
96
|
+
const resource = new SqliteSquare(location, artifact, clock, input.body, input.wakeAddress, wakeTransport, squareIndexPath(env), automaticDelivery, borrowed === undefined, input.participant);
|
|
97
|
+
await resource.publishIdentity();
|
|
98
|
+
return resource;
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
if (borrowed === undefined)
|
|
102
|
+
await artifact.close();
|
|
103
|
+
if (error instanceof InternalSquareError && error.code === 'not_found')
|
|
104
|
+
throw new SquareError('unavailable', `Square is unavailable at ${location}`);
|
|
105
|
+
throw error;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
static async build(input, control, automaticDelivery = true) {
|
|
109
|
+
checkControl(control);
|
|
110
|
+
const env = { ...(input.env ?? process.env) }, location = squareLocation({ ...input, env });
|
|
111
|
+
await createSquare(location, { hardCap: input.hardCap, throttlePerMinute: input.throttlePerMinute,
|
|
112
|
+
template: input.template, force: input.force ?? false }, input.markdown, control?.signal);
|
|
113
|
+
// Publication already committed; cancellation cannot erase the completed build.
|
|
114
|
+
return SqliteSquare.at({ ...input, path: location, env }, undefined, automaticDelivery);
|
|
115
|
+
}
|
|
116
|
+
static inMemory(input) {
|
|
117
|
+
const env = { ...(input.env ?? process.env) };
|
|
118
|
+
return new SqliteSquare('memory', createArtifactPort(':memory:', prepareSquareBuild(input, input.markdown)), squareClock(env, input.clock), input.body, input.wakeAddress, input.wakeTransport, undefined, true, true, input.participant);
|
|
119
|
+
}
|
|
120
|
+
/** Discovery is best effort and never grants write authority. Finish it before returning. */
|
|
121
|
+
async publishIdentity() {
|
|
122
|
+
if (this.location !== 'memory' && this.body !== undefined && this.indexPath !== undefined)
|
|
123
|
+
await rememberSquarePath(this.location, this.body, this.indexPath).catch(() => undefined);
|
|
124
|
+
}
|
|
125
|
+
observation(standingCount, held, participant, now = this.clock()) {
|
|
126
|
+
return { location: this.location, standingCount, held, now,
|
|
127
|
+
...(participant === undefined ? {} : { participant }), automaticDeliveryAvailable: this.automaticDelivery && this.body !== undefined };
|
|
128
|
+
}
|
|
129
|
+
async contextObservation(participant, control) {
|
|
130
|
+
return (await this.artifact.read(facts => this.observation(inSquareCount(facts), facts.hold().active, participant), control?.signal)).value;
|
|
131
|
+
}
|
|
132
|
+
handle(name, seat) { return new SqliteSquare.ParticipantHandle(name, this, seat); }
|
|
133
|
+
// The nested handle can use its resource's private dependencies without widening
|
|
134
|
+
// the concrete-derived backend contract or copying them into a second context.
|
|
135
|
+
static ParticipantHandle = class {
|
|
136
|
+
name;
|
|
137
|
+
owner;
|
|
138
|
+
seat;
|
|
139
|
+
constructor(name, owner, seat) {
|
|
140
|
+
this.name = name;
|
|
141
|
+
this.owner = owner;
|
|
142
|
+
this.seat = seat;
|
|
143
|
+
}
|
|
144
|
+
async express(bodyOrInput, optionsOrControl = {}, operationControl) {
|
|
145
|
+
const square = this.owner;
|
|
146
|
+
const input = typeof bodyOrInput === 'object' && bodyOrInput !== null ? bodyOrInput : undefined;
|
|
147
|
+
const body = typeof bodyOrInput === 'string' || bodyOrInput === undefined ? bodyOrInput : bodyOrInput.body;
|
|
148
|
+
const options = (input ?? optionsOrControl);
|
|
149
|
+
const control = input === undefined ? operationControl : optionsOrControl;
|
|
150
|
+
let name = this.name, effect = 'unsent';
|
|
151
|
+
let landedId;
|
|
152
|
+
let waited = false, heldWaitedMs = 0;
|
|
153
|
+
try {
|
|
154
|
+
checkControl(control);
|
|
155
|
+
name = (await square.artifact.read(facts => resolveKnownName(facts, this.name), control?.signal)).value;
|
|
156
|
+
const seat = requireSeat(this.seat, square.body, name);
|
|
157
|
+
const feed = !options.observe ? undefined : (await square.artifact.read(facts => {
|
|
158
|
+
const pendingPublic = directedSuffix(facts, name);
|
|
159
|
+
return { pendingPublic, sayNumbers: facts.sayOrdinals(pendingPublic.map(item => item.index)),
|
|
160
|
+
perceptions: Object.fromEntries(pendingPublic.map(item => [item.index, 'full'])) };
|
|
161
|
+
}, control?.signal)).value;
|
|
162
|
+
while (true) {
|
|
163
|
+
checkControl(control);
|
|
164
|
+
try {
|
|
165
|
+
effect = 'unknown';
|
|
166
|
+
const now = square.clock();
|
|
167
|
+
const reply = rejectUnsentActivity(() => {
|
|
168
|
+
try {
|
|
169
|
+
return options.reply === undefined ? undefined : parseRequiredActivityId(options.reply);
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
if (isSquareError(error))
|
|
173
|
+
error.facts = { ...error.facts, reason: 'invalid_reply', activityId: options.reply };
|
|
174
|
+
throw error;
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
const committed = (await square.artifact.transactAs(participantAuthority(seat), facts => {
|
|
178
|
+
// Decoding selected rows is storage work, not proof of safe admission refusal.
|
|
179
|
+
const proposed = decideAct(facts, { name, body, force: options.force ?? false, metadata: options.metadata, now, mentions: options.mentions,
|
|
180
|
+
...(options.reach === undefined ? {} : { reach: options.reach }), ...(options.noMention === undefined ? {} : { noMention: options.noMention }), ...(reply === undefined ? {} : { reply }) });
|
|
181
|
+
const holder = proposed.type === 'held' ? facts.hold().actor : undefined;
|
|
182
|
+
const decision = rejectUnsentActivity(() => {
|
|
183
|
+
if (proposed.type === 'blocked')
|
|
184
|
+
throw new SquareError('behind', `${participantIdentity(name)} has pending activity`, { pending: proposed.activitySummaries.reduce((count, summary) => count + summary.count, 0) });
|
|
185
|
+
if (proposed.type === 'held')
|
|
186
|
+
throw new SquareError('held', 'The square is held', { ...(holder === undefined ? {} : { holder }), ...(proposed.reason === undefined ? {} : { holdReason: proposed.reason }) });
|
|
187
|
+
if (proposed.type === 'capped')
|
|
188
|
+
throw new SquareError('capped', `${participantIdentity(name)} reached the activity cap`);
|
|
189
|
+
if (proposed.type === 'throttled')
|
|
190
|
+
throw new SquareError('throttled', `${name} is throttled`, { retryAfterMs: proposed.delayMs });
|
|
191
|
+
if (proposed.type === 'bell_quota')
|
|
192
|
+
throw new SquareError('bell_quota', `${participantIdentity(name)} cannot ring the bell yet`, { retryAfterMs: Math.max(1, proposed.nextAt - now) });
|
|
193
|
+
return proposed;
|
|
194
|
+
});
|
|
195
|
+
return { stored: facts.land(decision.act), standingCount: inSquareCount(facts), held: facts.hold().active,
|
|
196
|
+
ownActivityCount: facts.participant(name).sayCount, hardCap: facts.header().hardCap,
|
|
197
|
+
...(decision.unmatchedMentions === undefined ? {} : { unmatchedMentions: decision.unmatchedMentions }),
|
|
198
|
+
...(decision.replyAuthorNotHere === undefined ? {} : { replyAuthorNotHere: decision.replyAuthorNotHere }) };
|
|
199
|
+
}, control?.signal)).value;
|
|
200
|
+
landedId = formatActivityId(committed.stored.index);
|
|
201
|
+
effect = 'landed';
|
|
202
|
+
await square.publishIdentity();
|
|
203
|
+
const delivery = square.wakeTransport !== undefined && square.location !== 'memory'
|
|
204
|
+
? await deliverPending({ artifact: square.artifact, transport: square.wakeTransport, location: square.location, activity: committed.stored.index, now })
|
|
205
|
+
.catch(() => ({ attempted: 0, accepted: 0, failed: 0, unknown: 0, notCapable: 1 }))
|
|
206
|
+
: { attempted: 0, accepted: 0, failed: 0, unknown: 0, notCapable: 1 };
|
|
207
|
+
const { stored, standingCount, held, ownActivityCount, hardCap, ...metadata } = committed;
|
|
208
|
+
const hint = expressHint(ownActivityCount);
|
|
209
|
+
return { activity: toPublicActivity(stored), delivery, waited, ...metadata,
|
|
210
|
+
...(hint === undefined ? {} : { hint }), ...(feed === undefined ? {} : { observation: {
|
|
211
|
+
...square.observation(standingCount, held, name, now), ownActivityCount, hardCap, ...feed, participant: name,
|
|
212
|
+
} }) };
|
|
213
|
+
}
|
|
214
|
+
catch (error) {
|
|
215
|
+
if (effect !== 'landed' && isSquareError(error) && error.facts?.activityUnsent === true)
|
|
216
|
+
effect = 'unsent';
|
|
217
|
+
if (effect === 'landed' || !isSquareError(error) || options.noWait || (error.code !== 'held' && error.code !== 'throttled'))
|
|
218
|
+
throw error;
|
|
219
|
+
if (error.code === 'held' && heldWaitedMs >= HELD_WAIT_BUDGET_MS)
|
|
220
|
+
throw error;
|
|
221
|
+
waited = true;
|
|
222
|
+
control?.onProgress?.({ kind: 'waiting', reason: error.code,
|
|
223
|
+
...(error.code === 'throttled' ? { delayMs: error.facts?.retryAfterMs ?? 1 } : {}),
|
|
224
|
+
...(error.facts?.holder === undefined ? {} : { holder: error.facts.holder }),
|
|
225
|
+
...(error.facts?.holdReason === undefined ? {} : { holdReason: error.facts.holdReason }) });
|
|
226
|
+
if (error.code === 'throttled')
|
|
227
|
+
await sleep(error.facts?.retryAfterMs ?? 1, undefined, { signal: control?.signal });
|
|
228
|
+
else {
|
|
229
|
+
const started = Date.now();
|
|
230
|
+
const observation = await square.artifact.read(facts => facts.hold().active, control?.signal);
|
|
231
|
+
if (observation.value)
|
|
232
|
+
await square.artifact.changed(observation.version, Math.max(1, HELD_WAIT_BUDGET_MS - heldWaitedMs), control?.signal);
|
|
233
|
+
heldWaitedMs += Date.now() - started;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
catch (error) {
|
|
239
|
+
const recipient = isSquareError(error) && ['not_standing', 'bell_mention_conflict'].includes(error.facts?.reason ?? '');
|
|
240
|
+
throw failure(error, { operation: 'express', participant: name, subject: recipient ? 'recipient' : 'caller', expressEffect: effect,
|
|
241
|
+
...(landedId === undefined ? {} : { landedId }) });
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
async listener(verb, target, options, control) {
|
|
245
|
+
const square = this.owner;
|
|
246
|
+
const name = (await square.artifact.read(facts => resolveKnownName(facts, this.name), control?.signal)).value;
|
|
247
|
+
checkControl(control);
|
|
248
|
+
const seat = requireSeat(this.seat, square.body, name), now = square.clock();
|
|
249
|
+
return (await square.artifact.transactAs(participantAuthority(seat), facts => {
|
|
250
|
+
const act = verb === 'listen' ? coreListen(facts, name, target, now) : verb === 'ignore' ? coreIgnore(facts, name, target, now) : coreUnlisten(facts, name, target, now);
|
|
251
|
+
const activity = act === undefined ? null : toPublicActivity(facts.land(act));
|
|
252
|
+
return { activity, ...(!options.observe ? {} : { observation: {
|
|
253
|
+
...square.observation(inSquareCount(facts), facts.hold().active, name, now), participant: name,
|
|
254
|
+
requestedTarget: target, target: activity?.target ?? target,
|
|
255
|
+
} }) };
|
|
256
|
+
}, control?.signal)).value;
|
|
257
|
+
}
|
|
258
|
+
listen(target, options = {}, control) { return this.listener('listen', target, options, control); }
|
|
259
|
+
ignore(target, options = {}, control) { return this.listener('ignore', target, options, control); }
|
|
260
|
+
unlisten(target, options = {}, control) { return this.listener('unlisten', target, options, control); }
|
|
261
|
+
async listening(options = {}, control) {
|
|
262
|
+
const square = this.owner;
|
|
263
|
+
return (await square.artifact.read(facts => {
|
|
264
|
+
const name = resolveKnownName(facts, this.name);
|
|
265
|
+
return { listening: coreListening(facts, name), ...(!options.observe ? {} : { observation: square.observation(inSquareCount(facts), facts.hold().active, name) }) };
|
|
266
|
+
}, control?.signal)).value;
|
|
267
|
+
}
|
|
268
|
+
/** Receipt selection rechecks the lease inside the writer, not at its last heartbeat. */
|
|
269
|
+
async catchPage(name, seat, options, control, leaseId) {
|
|
270
|
+
return (await this.owner.artifact.transactAs(participantAuthority(seat), facts => {
|
|
271
|
+
if (leaseId !== undefined && facts.lease(name)?.leaseId !== leaseId)
|
|
272
|
+
return null;
|
|
273
|
+
return decideCatch(facts, name, options, this.owner.clock());
|
|
274
|
+
}, control?.signal)).value;
|
|
275
|
+
}
|
|
276
|
+
async catch(options = {}, control) {
|
|
277
|
+
const square = this.owner;
|
|
278
|
+
let name = this.name, subject = 'caller';
|
|
279
|
+
let leaseId, seat;
|
|
280
|
+
let waitStarted = false, replaced = false;
|
|
281
|
+
let selection = options;
|
|
282
|
+
const idle = options.idle ?? 0;
|
|
283
|
+
const complete = async (caught, stop) => {
|
|
284
|
+
const view = caught ?? (await square.artifact.read(facts => observeCatchSelection(facts, name, [], {
|
|
285
|
+
unseenOnly: true, senders: selection.from, ...(selection.mention === true ? { attentionOnly: true } : {}),
|
|
286
|
+
}, square.clock()))).value;
|
|
287
|
+
if (stop === 'empty-now' || stop === undefined)
|
|
288
|
+
stop = view.terminalStatus ?? stop;
|
|
289
|
+
if (leaseId !== undefined) {
|
|
290
|
+
await releaseWatchLease(square.artifact, seat, leaseId).catch(() => undefined);
|
|
291
|
+
leaseId = undefined;
|
|
292
|
+
}
|
|
293
|
+
let presence;
|
|
294
|
+
if (options.observe) {
|
|
295
|
+
try {
|
|
296
|
+
presence = (await square.artifact.read(facts => catchPresence(facts, square.clock()), control?.signal)).value;
|
|
297
|
+
}
|
|
298
|
+
catch { /* Optional presence cannot erase the completed selection or receipts. */ }
|
|
299
|
+
}
|
|
300
|
+
const status = stop === 'already-active' || stop === 'replaced' || stop === 'capped' || stop === 'quorum' ? stop : undefined;
|
|
301
|
+
return { activities: view.activities.map(activity => ({ ...toPublicActivity(activity), perception: 'full' })),
|
|
302
|
+
consumedThrough: view.consumedThrough, remaining: view.remaining, idleExpired: stop === 'stale',
|
|
303
|
+
...(status === undefined ? {} : { status }), ...(view.hint === undefined ? {} : { hint: view.hint }), ...(!options.observe ? {} : { observation: {
|
|
304
|
+
...square.observation(view.standingCount, view.held, view.participant, view.now), participant: view.participant,
|
|
305
|
+
sayNumbers: view.sayNumbers, ownActivityCount: view.ownActivityCount, hardCap: view.hardCap, stop,
|
|
306
|
+
...(presence === undefined ? { enrichmentUnavailable: true } : { presence }), waitStarted, replaced,
|
|
307
|
+
} }) };
|
|
308
|
+
};
|
|
309
|
+
try {
|
|
310
|
+
checkControl(control);
|
|
311
|
+
validateName(name);
|
|
312
|
+
if (!Number.isFinite(idle) || idle < 0)
|
|
313
|
+
throw new SquareError('invalid_args', 'Catch idle duration must be a non-negative number');
|
|
314
|
+
if (options.id !== undefined && ['idle', 'from', 'mention', 'limit', 'replace'].some(key => Object.hasOwn(options, key)))
|
|
315
|
+
throw new SquareError('invalid_args', 'Catch id cannot be combined with idle, from, mention, limit or replace');
|
|
316
|
+
if (Object.hasOwn(options, 'replace') && idle === 0)
|
|
317
|
+
throw new SquareError('invalid_args', 'Catch replacement requires a positive idle duration');
|
|
318
|
+
catchLimit(options.limit);
|
|
319
|
+
const probe = (await square.artifact.read(facts => {
|
|
320
|
+
name = resolveKnownName(facts, this.name);
|
|
321
|
+
seat = requireSeat(this.seat, square.body, name);
|
|
322
|
+
subject = 'filter';
|
|
323
|
+
const from = options.from?.map(target => resolveKnownName(facts, target));
|
|
324
|
+
selection = { ...options, ...(from === undefined ? {} : { from }) };
|
|
325
|
+
subject = 'caller';
|
|
326
|
+
if (idle !== 0 || options.id !== undefined)
|
|
327
|
+
return undefined;
|
|
328
|
+
const page = selectCatchPage(facts, name, selection);
|
|
329
|
+
return page.indexes.length === 0 ? observeCatchSelection(facts, page.participant, [], page.filter, square.clock()) : undefined;
|
|
330
|
+
}, control?.signal)).value;
|
|
331
|
+
if (idle === 0) {
|
|
332
|
+
const data = probe ?? (await this.catchPage(name, seat, selection, control));
|
|
333
|
+
return await complete(data, data.activities.length === 0 ? 'empty-now' : undefined);
|
|
334
|
+
}
|
|
335
|
+
const watch = {
|
|
336
|
+
...(selection.from === undefined ? {} : { participants: [...selection.from] }), ...(options.mention ? { mention: name } : {}),
|
|
337
|
+
...(options.replace === undefined ? {} : { replace: options.replace }),
|
|
338
|
+
};
|
|
339
|
+
const start = await acquireWatchLease(square.artifact, seat, square.clock(), crypto.randomUUID(), watch, control?.signal);
|
|
340
|
+
if (start.type === 'active')
|
|
341
|
+
return await complete(undefined, 'already-active');
|
|
342
|
+
leaseId = start.leaseId;
|
|
343
|
+
waitStarted = true;
|
|
344
|
+
replaced = start.replaced;
|
|
345
|
+
control?.onProgress?.({ kind: 'catch-started', participant: name, idleMs: idle, replacementRequested: options.replace ?? false, replaced });
|
|
346
|
+
let quietSince = square.clock(), wasHeld = false, nextHeartbeat = start.heartbeatAt + WATCH_HEARTBEAT_MS;
|
|
347
|
+
while (true) {
|
|
348
|
+
checkControl(control);
|
|
349
|
+
// Arm before selecting: a commit during selection must not be missed.
|
|
350
|
+
const baseline = await square.artifact.revision(control?.signal);
|
|
351
|
+
const pulse = await pulseWatchLease(square.artifact, seat, square.clock(), leaseId, watch, square.clock() >= nextHeartbeat, control?.signal);
|
|
352
|
+
if (pulse.type === 'replaced')
|
|
353
|
+
return await complete(undefined, 'replaced');
|
|
354
|
+
if (pulse.type === 'terminal')
|
|
355
|
+
return await complete(undefined, pulse.status);
|
|
356
|
+
if (pulse.heartbeatAt !== undefined)
|
|
357
|
+
nextHeartbeat = pulse.heartbeatAt + WATCH_HEARTBEAT_MS;
|
|
358
|
+
if (pulse.type === 'sleep') {
|
|
359
|
+
const caught = await this.catchPage(name, seat, selection, control, leaseId);
|
|
360
|
+
if (caught === null)
|
|
361
|
+
return await complete(undefined, 'replaced');
|
|
362
|
+
if (caught.activities.length > 0)
|
|
363
|
+
return await complete(caught);
|
|
364
|
+
}
|
|
365
|
+
if (pulse.type === 'held' || wasHeld)
|
|
366
|
+
quietSince = square.clock();
|
|
367
|
+
wasHeld = pulse.type === 'held';
|
|
368
|
+
if (!wasHeld && square.clock() - quietSince >= idle) {
|
|
369
|
+
const owns = await ownsWatchLease(square.artifact, seat, square.clock(), leaseId, control?.signal);
|
|
370
|
+
return await complete(undefined, owns ? 'stale' : 'replaced');
|
|
371
|
+
}
|
|
372
|
+
const deadline = Math.min(nextHeartbeat, wasHeld ? Infinity : quietSince + idle);
|
|
373
|
+
await square.artifact.changed(baseline, Math.max(0, deadline - square.clock()), control?.signal);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
catch (error) {
|
|
377
|
+
throw failure(error, { operation: 'catch', subject, participant: name });
|
|
378
|
+
}
|
|
379
|
+
finally {
|
|
380
|
+
// Cancellation releases only this captured generation and this lease.
|
|
381
|
+
if (leaseId !== undefined)
|
|
382
|
+
await releaseWatchLease(square.artifact, seat, leaseId).catch(() => undefined);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
history(query, control) { return this.owner.history(query, control); }
|
|
386
|
+
async lifecycle(verb, body, options, control) {
|
|
387
|
+
if (verb === 'done')
|
|
388
|
+
validateDoneBody(body);
|
|
389
|
+
const square = this.owner;
|
|
390
|
+
const name = (await square.artifact.read(facts => resolveKnownName(facts, this.name), control?.signal)).value;
|
|
391
|
+
const seat = requireSeat(this.seat, square.body, name), now = square.clock();
|
|
392
|
+
return (await square.artifact.transactAs(participantAuthority(seat), facts => {
|
|
393
|
+
const stored = facts.land(verb === 'done' ? coreDone(facts, name, body ?? '', now) : verb === 'hold' ? coreHold(facts, name, body ?? '', now) : coreResume(facts, name, now));
|
|
394
|
+
return { activity: toPublicActivity(stored), ...(!options.observe ? {} : { observation: {
|
|
395
|
+
...square.observation(inSquareCount(facts), facts.hold().active, name, now), activity: stored,
|
|
396
|
+
} }) };
|
|
397
|
+
}, control?.signal)).value;
|
|
398
|
+
}
|
|
399
|
+
hold(reason, options = {}, control) { return this.lifecycle('hold', reason, options, control); }
|
|
400
|
+
resume(options = {}, control) { return this.lifecycle('resume', undefined, options, control); }
|
|
401
|
+
done(body, options = {}, control) { return this.lifecycle('done', body, options, control); }
|
|
402
|
+
};
|
|
403
|
+
async participant(requested) {
|
|
404
|
+
const selected = requested ?? this.defaultParticipant;
|
|
405
|
+
if (selected !== undefined) {
|
|
406
|
+
validateName(selected);
|
|
407
|
+
const seat = this.body === undefined ? undefined : (await this.artifact.read(facts => facts.seat(selected))).value;
|
|
408
|
+
return this.handle(selected, seat);
|
|
409
|
+
}
|
|
410
|
+
if (this.body !== undefined) {
|
|
411
|
+
const seats = (await this.artifact.read(facts => facts.seatsForBody(this.body))).value;
|
|
412
|
+
if (seats.length === 1)
|
|
413
|
+
return this.handle(seats[0].participant, seats[0]);
|
|
414
|
+
if (seats.length > 1)
|
|
415
|
+
throw new SquareError('invalid_args', 'Square needs an explicit participant name: this body holds multiple names');
|
|
416
|
+
}
|
|
417
|
+
throw new SquareError('invalid_args', 'Square needs one unambiguous participant name');
|
|
418
|
+
}
|
|
419
|
+
async join(requested, options = {}, control) {
|
|
420
|
+
let name = requested;
|
|
421
|
+
try {
|
|
422
|
+
checkControl(control);
|
|
423
|
+
name = requireJoinName(requested);
|
|
424
|
+
const last = options.last ?? HISTORY_DEFAULT_LIMIT;
|
|
425
|
+
if (!Number.isSafeInteger(last) || last < 1 || last > HISTORY_MAX_LIMIT)
|
|
426
|
+
throw new SquareError('invalid_args', 'Join history limit must be between 1 and 100');
|
|
427
|
+
const body = requireBody(this.body), now = this.clock();
|
|
428
|
+
const initial = !options.takeover ? undefined : (await this.artifact.read(facts => facts.participant(name)?.joined ? facts.seat(name) : undefined, control?.signal)).value;
|
|
429
|
+
const authority = initial === undefined ? { kind: 'join', participant: name, body, wakeAddress: this.wakeAddress }
|
|
430
|
+
: { kind: 'takeover', participant: initial.participant, body, expectedGeneration: initial.generation, wakeAddress: this.wakeAddress };
|
|
431
|
+
const committed = (await this.artifact.transactAs(authority, facts => {
|
|
432
|
+
const participant = facts.participant(name)?.name ?? name;
|
|
433
|
+
const current = facts.participant(participant), currentSeat = facts.seat(participant);
|
|
434
|
+
let kind = 'joined', activity = null;
|
|
435
|
+
if (initial !== undefined) {
|
|
436
|
+
if (!current?.joined)
|
|
437
|
+
throw new SquareError('already_done', `${participantIdentity(participant)} is no longer standing here`);
|
|
438
|
+
facts.land(coreDone(facts, participant, '', now));
|
|
439
|
+
kind = 'taken-over';
|
|
440
|
+
}
|
|
441
|
+
else if (current?.joined) {
|
|
442
|
+
if (currentSeat === undefined || !sameBody(currentSeat.body, body))
|
|
443
|
+
throw new SquareError('already_joined', `${participantIdentity(participant)} is held by another body; take it over explicitly`);
|
|
444
|
+
if (this.wakeAddress !== undefined && canRefreshWakeAddress(currentSeat.wakeAddress, this.wakeAddress))
|
|
445
|
+
facts.updateWakeAddress(asSeatRef(currentSeat), this.wakeAddress);
|
|
446
|
+
kind = 'reconnected';
|
|
447
|
+
}
|
|
448
|
+
if (kind !== 'reconnected') {
|
|
449
|
+
const decision = decideJoin(facts, participant, now);
|
|
450
|
+
if (decision.joinAct === undefined)
|
|
451
|
+
throw new SquareError('already_joined', `${participantIdentity(participant)} could not be joined`);
|
|
452
|
+
activity = toPublicActivity(facts.land(decision.joinAct));
|
|
453
|
+
}
|
|
454
|
+
const seat = facts.seat(participant);
|
|
455
|
+
if (seat === undefined)
|
|
456
|
+
throw new SquareError('invalid_args', 'The join activity did not establish its artifact seat.');
|
|
457
|
+
return { participant: this.handle(seat.participant, seat), activity, kind,
|
|
458
|
+
participantCount: facts.roster().filter(item => item.joined || item.done).length,
|
|
459
|
+
...(!options.observe ? {} : { observation: this.observation(inSquareCount(facts), facts.hold().active, seat.participant, now) }) };
|
|
460
|
+
}, control?.signal)).value;
|
|
461
|
+
await this.publishIdentity();
|
|
462
|
+
if (!options.observe)
|
|
463
|
+
return committed;
|
|
464
|
+
let entry;
|
|
465
|
+
try {
|
|
466
|
+
entry = (await this.artifact.read(facts => entryPresentation(facts, committed.participant.name, last), control?.signal)).value;
|
|
467
|
+
}
|
|
468
|
+
catch { /* Keep the committed join even when optional history is unavailable. */ }
|
|
469
|
+
return { ...committed, observation: { ...committed.observation, ...(entry === undefined ? { enrichmentUnavailable: true } : { entry }) } };
|
|
470
|
+
}
|
|
471
|
+
catch (error) {
|
|
472
|
+
throw failure(error, { operation: 'join', subject: 'caller', participant: name });
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
async implicitJoin(name, control) {
|
|
476
|
+
checkControl(control);
|
|
477
|
+
validateName(name);
|
|
478
|
+
const body = requireBody(this.body), now = this.clock();
|
|
479
|
+
const result = (await this.artifact.transactAs({ kind: 'join', participant: name, body, wakeAddress: this.wakeAddress }, facts => {
|
|
480
|
+
const decision = decideImplicitJoin(facts, name, now);
|
|
481
|
+
let seat = facts.seat(decision.joinedName);
|
|
482
|
+
if (decision.joinAct === undefined) {
|
|
483
|
+
if (decision.state === 'active' && (seat === undefined || !sameBody(seat.body, body)))
|
|
484
|
+
throw new SquareError('already_joined', `${participantIdentity(decision.joinedName)} is held by another body`);
|
|
485
|
+
if (decision.state === 'active' && this.wakeAddress !== undefined && canRefreshWakeAddress(seat.wakeAddress, this.wakeAddress))
|
|
486
|
+
facts.updateWakeAddress(asSeatRef(seat), this.wakeAddress);
|
|
487
|
+
}
|
|
488
|
+
else {
|
|
489
|
+
facts.land(decision.joinAct);
|
|
490
|
+
seat = facts.seat(decision.joinedName);
|
|
491
|
+
if (seat === undefined)
|
|
492
|
+
throw new SquareError('invalid_args', 'The implicit join did not establish its artifact seat.');
|
|
493
|
+
}
|
|
494
|
+
return { state: decision.state, ...(decision.state === 'done' ? {} : { participant: this.handle(decision.joinedName, seat) }) };
|
|
495
|
+
}, control?.signal)).value;
|
|
496
|
+
if (result.participant !== undefined)
|
|
497
|
+
await this.publishIdentity();
|
|
498
|
+
return result;
|
|
499
|
+
}
|
|
500
|
+
async snapshot(control) { return (await this.artifact.read(facts => snapshot(facts, this.clock()), control?.signal)).value; }
|
|
501
|
+
async history(query = {}, control) {
|
|
502
|
+
if (query.limit !== undefined && query.limit !== null && (!Number.isSafeInteger(query.limit) || query.limit < 1 || query.limit > HISTORY_MAX_LIMIT))
|
|
503
|
+
throw new SquareError('invalid_args', `History limit must be a positive integer no greater than ${HISTORY_MAX_LIMIT}`);
|
|
504
|
+
if (query.grep !== undefined && query.fixed !== undefined)
|
|
505
|
+
throw new SquareError('invalid_args', 'History cannot combine regular-expression and literal search');
|
|
506
|
+
if (query.grep === '' || query.fixed === '')
|
|
507
|
+
throw new SquareError('invalid_args', 'History search requires non-empty text');
|
|
508
|
+
if (query.since !== undefined && !Number.isFinite(query.since))
|
|
509
|
+
throw new SquareError('invalid_args', 'History timestamp must be finite');
|
|
510
|
+
for (const count of [query.contextBefore, query.contextAfter])
|
|
511
|
+
if (count !== undefined && (!Number.isSafeInteger(count) || count < 0))
|
|
512
|
+
throw new SquareError('invalid_args', 'History context must be a non-negative safe integer');
|
|
513
|
+
const limit = query.limit === undefined ? (query.at?.length ? null : HISTORY_DEFAULT_LIMIT) : query.limit;
|
|
514
|
+
const contextual = !!query.at?.length || query.contextBefore !== undefined || query.contextAfter !== undefined;
|
|
515
|
+
const kinds = contextual || query.since !== undefined ? undefined : NON_READ;
|
|
516
|
+
return (await this.artifact.read(facts => {
|
|
517
|
+
const selected = selectHistory(facts, query, limit, kinds, query.observe === true);
|
|
518
|
+
if (contextual && limit === null && selected.totalMatches > HISTORY_MAX_LIMIT)
|
|
519
|
+
throw failure(new SquareError('invalid_args', `History context exceeds ${HISTORY_MAX_LIMIT} activities`), { operation: 'history', historyBounds: { maxLimit: HISTORY_MAX_LIMIT, totalMatches: selected.totalMatches } });
|
|
520
|
+
const activities = publicActivities(selected.activities);
|
|
521
|
+
if (!query.observe)
|
|
522
|
+
return { activities };
|
|
523
|
+
const indexes = selected.activities.map(activity => activity.index), presenceAnchors = {};
|
|
524
|
+
for (const participant of facts.roster()) {
|
|
525
|
+
const anchor = presenceAnchor(facts, participant.name);
|
|
526
|
+
if (indexes.includes(anchor))
|
|
527
|
+
(presenceAnchors[anchor] ??= []).push(participant.name);
|
|
528
|
+
}
|
|
529
|
+
return { activities, observation: { ...this.observation(inSquareCount(facts), facts.hold().active), ...selected,
|
|
530
|
+
sayNumbers: facts.sayOrdinals(indexes), presenceAnchors } };
|
|
531
|
+
}, control?.signal)).value;
|
|
532
|
+
}
|
|
533
|
+
async status(options = {}, control) {
|
|
534
|
+
const viewer = !options.observe ? '' : options.viewer ?? (await this.participant().catch(() => undefined))?.name ?? '';
|
|
535
|
+
return (await this.artifact.read(facts => queryStatus(facts, {
|
|
536
|
+
location: this.location, now: this.clock(), automaticDeliveryAvailable: this.automaticDelivery && this.body !== undefined,
|
|
537
|
+
}, viewer, options.observe === true), control?.signal)).value;
|
|
538
|
+
}
|
|
539
|
+
async participants(options = {}, control) {
|
|
540
|
+
return (await this.artifact.read(facts => queryParticipants(facts, {
|
|
541
|
+
location: this.location, now: this.clock(), automaticDeliveryAvailable: this.automaticDelivery && this.body !== undefined,
|
|
542
|
+
}, options.observe === true), control?.signal)).value;
|
|
543
|
+
}
|
|
544
|
+
async recovery(requested, control) {
|
|
545
|
+
const name = (await this.participant(requested).catch(() => undefined))?.name;
|
|
546
|
+
const facts = name === undefined ? undefined : (await this.artifact.read(facts => recoveryObservation(facts, name, this.clock()), control?.signal).catch(() => undefined))?.value;
|
|
547
|
+
return facts ?? await this.contextObservation(name, control);
|
|
548
|
+
}
|
|
549
|
+
stream(options, control) {
|
|
550
|
+
// A path stream follows replacements independently of this connection's inode
|
|
551
|
+
// and lifetime. Only explicit artifact readers borrow an existing port.
|
|
552
|
+
return streamSquare(this.location, options, control);
|
|
553
|
+
}
|
|
554
|
+
async recognize() {
|
|
555
|
+
if (this.body === undefined)
|
|
556
|
+
return [];
|
|
557
|
+
const candidates = (await this.artifact.read(facts => facts.seatsForBody(this.body))).value;
|
|
558
|
+
return candidates.map(seat => this.handle(seat.participant, seat));
|
|
559
|
+
}
|
|
560
|
+
close() { return this.ownsArtifact ? this.artifact.close() : Promise.resolve(); }
|
|
561
|
+
async cancelDeliveries(name, expectedGeneration) {
|
|
562
|
+
const body = this.body;
|
|
563
|
+
if (body === undefined)
|
|
564
|
+
return false;
|
|
565
|
+
const seat = (await this.artifact.read(facts => facts.seat(name))).value;
|
|
566
|
+
if (seat === undefined || seat.generation !== expectedGeneration || !sameBody(seat.body, body))
|
|
567
|
+
return false;
|
|
568
|
+
return this.artifact.cancelDeliveries(asSeatRef(seat));
|
|
569
|
+
}
|
|
570
|
+
async endOwnedSession(name, sessionId, expectedGeneration, body) {
|
|
571
|
+
const captured = (await this.artifact.read(facts => facts.seat(name))).value;
|
|
572
|
+
if (captured === undefined || captured.body.session !== sessionId || (body !== undefined && !sameBody(captured.body, body))
|
|
573
|
+
|| (expectedGeneration !== undefined && captured.generation !== expectedGeneration))
|
|
574
|
+
return null;
|
|
575
|
+
return (await this.artifact.transactAs(participantAuthority(captured), facts => {
|
|
576
|
+
const current = facts.seat(name);
|
|
577
|
+
if (current === undefined || current.generation !== captured.generation || !sameBody(current.body, captured.body))
|
|
578
|
+
return null;
|
|
579
|
+
return { activity: toPublicActivity(facts.land(coreDone(facts, current.participant, '', this.clock()))) };
|
|
580
|
+
})).value;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
export function diagnoseSquare(input, control) { checkControl(control); return diagnoseSquareFile(localSquareLocation(input)); }
|
|
584
|
+
export function migrateSquare(input, control) { checkControl(control); const env = input.env ?? process.env; return migrateSquareFile(localSquareLocation(input), control?.signal, env); }
|
|
585
|
+
export function discover(input, options = {}, control) {
|
|
586
|
+
if (input.path !== undefined || (input.env ?? process.env).SQUARE_LOCATION)
|
|
587
|
+
localSquareLocation(input);
|
|
588
|
+
const env = { ...(input.env ?? process.env) };
|
|
589
|
+
return discoverSquares(input.cwd ?? process.cwd(), options, control, { clock: squareClock(env, input.clock) });
|
|
590
|
+
}
|
|
591
|
+
export async function inbox(input, identity, options = {}, control) {
|
|
592
|
+
checkControl(control);
|
|
593
|
+
const env = { ...(input.env ?? process.env) }, limit = options.limit ?? 20;
|
|
594
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100)
|
|
595
|
+
throw new SquareError('invalid_args', 'Inbox limit must be between 1 and 100');
|
|
596
|
+
const query = typeof identity === 'string' ? { sessionId: identity, env } : { body: identity, env };
|
|
597
|
+
const rows = await sessionInboxCounts(query, control?.signal);
|
|
598
|
+
checkControl(control);
|
|
599
|
+
const compare = (left, right) => left < right ? -1 : left > right ? 1 : 0;
|
|
600
|
+
rows.sort((a, b) => compare(a.squarePath, b.squarePath) || compare(nameKey(a.name), nameKey(b.name)) || compare(a.name, b.name));
|
|
601
|
+
return { rows: rows.slice(0, limit), total: rows.length };
|
|
602
|
+
}
|