@astrosheep/square 0.3.69 → 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/harness-claude.js +1 -1
- package/dist/harness-codex.js +1 -1
- package/dist/harness-pi.js +1 -1
- 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/{opencode-delivery.d.ts → plugins/opencode/delivery.d.ts} +1 -1
- package/dist/{opencode-delivery.js → plugins/opencode/delivery.js} +18 -18
- package/dist/{opencode.d.ts → plugins/opencode/index.d.ts} +2 -2
- package/dist/{opencode.js → plugins/opencode/index.js} +9 -9
- 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/pi-native-delivery.md +1 -1
- 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 +5 -7
- package/{claude-plugin → plugins/claude}/.claude-plugin/plugin.json +1 -1
- package/{codex-plugin → plugins/codex}/.codex-plugin/plugin.json +1 -1
- package/plugins/opencode/delivery.ts +149 -0
- package/plugins/opencode/index.ts +136 -0
- package/plugins/paseo/OVERVIEW.md +14 -0
- package/plugins/paseo/README.md +54 -0
- package/plugins/paseo/client/square-message.tsx +106 -0
- package/plugins/paseo/client/transform.ts +24 -0
- package/plugins/paseo/index.client.tsx +19 -0
- package/plugins/paseo/package.json +19 -0
- package/plugins/paseo/paseo-plugin.json +7 -0
- package/plugins/paseo/shared/message.ts +29 -0
- package/{extensions → plugins/pi}/square-pi.js +8 -5
- 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
- /package/{claude-plugin → plugins/claude}/hooks/hooks.json +0 -0
- /package/{claude-plugin → plugins/claude}/hooks/register.js +0 -0
- /package/{claude-plugin → plugins/claude}/skills/square/SKILL.md +0 -0
- /package/{claude-plugin → plugins/claude}/types.d.ts +0 -0
- /package/{codex-plugin → plugins/codex}/hooks/hooks.json +0 -0
package/dist/presentation.js
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
|
-
import { sameName } from './model.js';
|
|
1
|
+
import { sameName, isSquareError } from './model.js';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { participantIdentity } from './participant-identity.js';
|
|
5
5
|
export { participantIdentity } from './participant-identity.js';
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import { actId, sayNumberFor, HELD_WAIT_BUDGET_MS } from './runtime.js';
|
|
6
|
+
import { formatActivityId, MAX_IDENTITY_SET_SIZE } from './square-core.js';
|
|
7
|
+
import { HELD_WAIT_BUDGET_MS } from './runtime.js';
|
|
9
8
|
import { formatDuration, formatRelativeTime, formatTimestamp } from './time.js';
|
|
10
|
-
import { compareParticipantActivity } from './decisions.js';
|
|
11
9
|
import { grepSnippet } from './search.js';
|
|
12
10
|
import { style } from './tty-style.js';
|
|
13
11
|
export function displayPath(squarePath, cwd = process.cwd()) {
|
|
@@ -39,6 +37,11 @@ export function formatRefusal(squarePath, bodyLines, opts = {}) {
|
|
|
39
37
|
return body === '' ? '' : `${body}\n`;
|
|
40
38
|
return withPathOutput(squarePath, body, { ...opts, plain: true });
|
|
41
39
|
}
|
|
40
|
+
export function refusalText(error) {
|
|
41
|
+
const text = error instanceof Error ? error.message : String(error);
|
|
42
|
+
const marked = isSquareError(error) && ['never_joined', 'not_standing', 'unmatched_mention'].includes(error.facts?.reason ?? '');
|
|
43
|
+
return marked ? text.split('\n').map((line) => `✕ ${line}`).join('\n') : text;
|
|
44
|
+
}
|
|
42
45
|
export function quoteShell(value) {
|
|
43
46
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
44
47
|
}
|
|
@@ -69,11 +72,7 @@ function formatAge(ms) {
|
|
|
69
72
|
return `${Math.max(0, ms)}ms`;
|
|
70
73
|
return `${Math.floor(ms / 1000)}s`;
|
|
71
74
|
}
|
|
72
|
-
|
|
73
|
-
export function renderPresenceLines(participants, now, limit = 5) {
|
|
74
|
-
const recent = participants
|
|
75
|
-
.filter((p) => p.state === 'done' || p.presence === 'watching' || (p.lastActiveAt !== undefined && now - p.lastActiveAt <= PRESENCE_WINDOW_MS))
|
|
76
|
-
.sort(compareParticipantActivity);
|
|
75
|
+
export function renderPresenceLines(recent, now, limit = 5) {
|
|
77
76
|
const shown = recent.slice(0, limit);
|
|
78
77
|
if (shown.length === 0)
|
|
79
78
|
return [style('dim', ' ○ nobody nearby')];
|
|
@@ -137,9 +136,11 @@ export function renderRoomChangeText(event) {
|
|
|
137
136
|
case 'resume':
|
|
138
137
|
return `${identity} lowered the hand`;
|
|
139
138
|
case 'listen':
|
|
140
|
-
return `${identity} turned an ear toward ${participantIdentity(event.target)}`;
|
|
139
|
+
return `${identity} turned an ear toward ${participantIdentity(event.target ?? 'someone')}`;
|
|
141
140
|
case 'ignore':
|
|
142
|
-
return `${identity} turned away from ${participantIdentity(event.target)}`;
|
|
141
|
+
return `${identity} turned away from ${participantIdentity(event.target ?? 'someone')}`;
|
|
142
|
+
default:
|
|
143
|
+
throw new Error('Expected a lifecycle activity');
|
|
143
144
|
}
|
|
144
145
|
}
|
|
145
146
|
function renderedBody(body, maxChars) {
|
|
@@ -152,6 +153,12 @@ function bodySuffix(body) {
|
|
|
152
153
|
return '';
|
|
153
154
|
return `\n${body.split('\n').map((line) => ` ${line}`).join('\n')}`;
|
|
154
155
|
}
|
|
156
|
+
function eventId(event) {
|
|
157
|
+
return 'index' in event ? formatActivityId(event.index) : event.id;
|
|
158
|
+
}
|
|
159
|
+
function replyId(reply) {
|
|
160
|
+
return typeof reply === 'number' ? formatActivityId(reply) : reply;
|
|
161
|
+
}
|
|
155
162
|
function renderEventCliFacts(event, opts = {}) {
|
|
156
163
|
const now = opts.now;
|
|
157
164
|
const maxBody = opts.preview;
|
|
@@ -168,17 +175,17 @@ function renderEventCliFacts(event, opts = {}) {
|
|
|
168
175
|
case 'say': {
|
|
169
176
|
const body = renderedBody(event.body, maxBody);
|
|
170
177
|
const mention = opts.mention;
|
|
171
|
-
const mentionSuffix = mention !== undefined &&
|
|
178
|
+
const mentionSuffix = mention !== undefined && (event.reach === 'bell' || event.mentions?.some((name) => sameName(name, mention)))
|
|
172
179
|
? ` · calls your name across the square — ${participantIdentity(mention)}`
|
|
173
180
|
: '';
|
|
174
|
-
const replySuffix = event.reply === undefined ? '' : ` · replies to ${
|
|
175
|
-
const meta = ` #${opts.actNumber ?? 1} · ${
|
|
181
|
+
const replySuffix = event.reply === undefined ? '' : ` · replies to ${replyId(event.reply)}`;
|
|
182
|
+
const meta = ` #${opts.actNumber ?? 1} · ${eventId(event)} · ${formatRelativeTime(event.at, now)}${mentionSuffix}${replySuffix}`;
|
|
176
183
|
return { text: `● ${participantIdentity(event.actor)}${style('dim', meta)}${bodySuffix(body.text)}`, bodyClipped: body.clipped };
|
|
177
184
|
}
|
|
178
185
|
case 'done': {
|
|
179
186
|
const body = renderedBody(event.body, maxBody);
|
|
180
187
|
return {
|
|
181
|
-
text: `${style('dim', '○')} ${participantIdentity(event.actor)} stepped out of the square — done${style('dim', ` · ${
|
|
188
|
+
text: `${style('dim', '○')} ${participantIdentity(event.actor)} stepped out of the square — done${style('dim', ` · ${eventId(event)} · ${formatRelativeTime(event.at, now)}`)}${bodySuffix(body.text)}`,
|
|
182
189
|
bodyClipped: body.clipped,
|
|
183
190
|
};
|
|
184
191
|
}
|
|
@@ -190,9 +197,8 @@ export function renderEventCli(event, opts = {}) {
|
|
|
190
197
|
return renderEventCliFacts(event, opts).text;
|
|
191
198
|
}
|
|
192
199
|
function renderPresenceOnlySay(event, opts = {}) {
|
|
193
|
-
const
|
|
194
|
-
const
|
|
195
|
-
const meta = style('dim', ` #${opts.actNumber ?? 1} · ${actId(event)} · ${formatRelativeTime(event.at, opts.now)}`);
|
|
200
|
+
const targets = event.reach === 'bell' ? [] : event.mentions ?? [];
|
|
201
|
+
const meta = style('dim', ` #${opts.actNumber ?? 1} · ${eventId(event)} · ${formatRelativeTime(event.at, opts.now)}`);
|
|
196
202
|
if (targets.length === 0) {
|
|
197
203
|
return `● ${participantIdentity(event.actor)}${meta}`;
|
|
198
204
|
}
|
|
@@ -207,10 +213,9 @@ function renderPresenceOnlySay(event, opts = {}) {
|
|
|
207
213
|
export function renderAmbientEventFacts(event, viewer, opts = {}) {
|
|
208
214
|
if (event.kind !== 'say')
|
|
209
215
|
return renderEventCliFacts(event, opts);
|
|
210
|
-
if (opts.perception === undefined
|
|
211
|
-
throw new Error('Ambient say rendering requires a settled perception
|
|
212
|
-
|
|
213
|
-
const seen = opts.perception ?? perceiveActivity(opts.squareState, event, viewer);
|
|
216
|
+
if (opts.perception === undefined)
|
|
217
|
+
throw new Error('Ambient say rendering requires a settled perception');
|
|
218
|
+
const seen = opts.perception;
|
|
214
219
|
if (seen === 'presence') {
|
|
215
220
|
const presence = renderPresenceOnlySay(event, opts);
|
|
216
221
|
const body = opts.presencePreview === undefined ? { text: '', clipped: false } : renderedBody(event.body, opts.presencePreview);
|
|
@@ -248,17 +253,8 @@ function renderUnreadSummary(opts) {
|
|
|
248
253
|
...(remainingSummaries === 0 ? [] : [style('dim', ` · ${remainingSummaries} more participants have unread activity`)]),
|
|
249
254
|
];
|
|
250
255
|
}
|
|
251
|
-
export function renderPendingFeed(
|
|
252
|
-
|
|
253
|
-
for (const act of publicItems) {
|
|
254
|
-
const rendered = renderAmbientEvent(act, viewer, {
|
|
255
|
-
actNumber: act.kind === 'say' ? sayNumberFor(history, act) : undefined,
|
|
256
|
-
...(squareState === undefined ? {} : { squareState }),
|
|
257
|
-
});
|
|
258
|
-
if (rendered !== '')
|
|
259
|
-
lines.push(rendered);
|
|
260
|
-
}
|
|
261
|
-
return lines.join('\n\n');
|
|
256
|
+
export function renderPendingFeed(publicItems, sayNumbers, perceptions, viewer) {
|
|
257
|
+
return publicItems.map((act) => renderAmbientEvent(act, viewer, { actNumber: sayNumbers[act.index], perception: perceptions[act.index] })).filter(Boolean).join('\n\n');
|
|
262
258
|
}
|
|
263
259
|
export function renderActivityBlocked(opts) {
|
|
264
260
|
const readNowCommand = `${participantCommandPrefix(opts.squarePath, opts.name)} catch`;
|
|
@@ -344,7 +340,7 @@ export function renderGrepActivitiesView(visible, totalMatches, noTruncate, squa
|
|
|
344
340
|
let truncated = false;
|
|
345
341
|
for (const act of publicVisible) {
|
|
346
342
|
const rawBody = act.body ?? '';
|
|
347
|
-
const header = `${style('dim',
|
|
343
|
+
const header = `${style('dim', formatActivityId(act.index))} · ${act.actor === undefined ? 'unknown' : participantIdentity(act.actor)} · ${style('dim', formatTimestamp(act.at))}`;
|
|
348
344
|
if (perception?.(act) === 'presence') {
|
|
349
345
|
chunks.push(header);
|
|
350
346
|
continue;
|
|
@@ -370,10 +366,10 @@ export function renderGrepActivitiesView(visible, totalMatches, noTruncate, squa
|
|
|
370
366
|
chunks.push(`${header}\n ${text.trim()}${omitted}`);
|
|
371
367
|
}
|
|
372
368
|
if (publicVisible.length === 1) {
|
|
373
|
-
chunks.push(`${commandPrefix(squarePath)} history --at ${
|
|
369
|
+
chunks.push(`${commandPrefix(squarePath)} history --at ${formatActivityId(publicVisible[0].index)} -C 2${truncated ? ' --no-truncate' : ''}`);
|
|
374
370
|
}
|
|
375
371
|
else if (truncated && publicVisible.length > 1) {
|
|
376
|
-
chunks.push(`${commandPrefix(squarePath)} history --at ${
|
|
372
|
+
chunks.push(`${commandPrefix(squarePath)} history --at ${formatActivityId(publicVisible[0].index)} -C 2 --no-truncate`);
|
|
377
373
|
}
|
|
378
374
|
return chunks.join('\n\n');
|
|
379
375
|
}
|
|
@@ -447,20 +443,3 @@ export function renderDoctorClean() {
|
|
|
447
443
|
export function renderDoctorUnfixable(reason) {
|
|
448
444
|
return ['✕ unreadable artifact', ` · ${truncateExternalDiagnostic(reason)}`].join('\n');
|
|
449
445
|
}
|
|
450
|
-
export function renderWatchOutput(history, publicItems, opts) {
|
|
451
|
-
const sections = [];
|
|
452
|
-
if (publicItems.length > 0) {
|
|
453
|
-
const rendered = publicItems
|
|
454
|
-
.map((act) => renderAmbientEvent(act, opts.viewer, {
|
|
455
|
-
actNumber: act.kind === 'say' ? sayNumberFor(history, act) : undefined,
|
|
456
|
-
mention: opts.mention,
|
|
457
|
-
...(opts.perceptions?.has(act.index) ? { perception: opts.perceptions.get(act.index) } : {}),
|
|
458
|
-
...(opts.squareState === undefined ? {} : { squareState: opts.squareState }),
|
|
459
|
-
}))
|
|
460
|
-
.filter(Boolean)
|
|
461
|
-
.join('\n\n');
|
|
462
|
-
if (rendered !== '')
|
|
463
|
-
sections.push(rendered);
|
|
464
|
-
}
|
|
465
|
-
return sections.join('\n\n') + '\n';
|
|
466
|
-
}
|
package/dist/routes.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type WakeRoute, type WakeRouteKind } from './model.js';
|
|
2
|
+
import type { SquareRead, SquareWrite } from './ports.js';
|
|
2
3
|
export { WAKE_ROUTE_KINDS } from './model.js';
|
|
3
4
|
export type { WakeRoute, WakeRouteKind } from './model.js';
|
|
4
5
|
export declare const ROUTE_FRESH_MS: number;
|
|
@@ -18,25 +19,17 @@ export declare function routeIdentityKey(route: Pick<WakeRoute, 'location' | 'pa
|
|
|
18
19
|
export type RouteEpoch = {
|
|
19
20
|
readonly epoch?: number;
|
|
20
21
|
};
|
|
21
|
-
export declare function
|
|
22
|
-
export declare function
|
|
23
|
-
/**
|
|
24
|
-
|
|
25
|
-
*
|
|
26
|
-
* A route survives when the ending transaction did not commit `done` and this session
|
|
27
|
-
* still holds the sole live presence for its participant — an in-flight same-session
|
|
28
|
-
* rejoin. `liveParticipants === undefined` means the ledger could not be read, so
|
|
29
|
-
* nothing is dropped on suspicion. An explicit epoch fence additionally keeps routes
|
|
30
|
-
* published by a later generation.
|
|
31
|
-
*/
|
|
32
|
-
export declare function dropEndedSessionWakeRoutesFromState(state: import('./model.js').SquareState, location: string, sessionId: string, opts: {
|
|
22
|
+
export declare function applyWakeRoute(facts: SquareWrite, route: Omit<WakeRoute, 'updatedAt'> & RouteEpoch, at?: number): void;
|
|
23
|
+
export declare function dropSessionWakeRoutes(facts: SquareWrite, location: string, sessionId: string, expectedEpoch?: number): void;
|
|
24
|
+
/** Keep an in-flight same-session rejoin; unknown host evidence is not a reason to retire it. */
|
|
25
|
+
export declare function dropEndedSessionWakeRoutes(facts: SquareWrite, location: string, sessionId: string, opts: {
|
|
33
26
|
readonly expectedEpoch?: number;
|
|
34
27
|
readonly liveParticipants?: ReadonlySet<string>;
|
|
35
28
|
readonly force: boolean;
|
|
36
29
|
readonly participant?: string;
|
|
37
30
|
}): void;
|
|
38
|
-
export declare function
|
|
39
|
-
export declare function sessionCanEndParticipant(
|
|
31
|
+
export declare function dropParticipantWakeRoutes(facts: SquareWrite, location: string, participant: string): void;
|
|
32
|
+
export declare function sessionCanEndParticipant(facts: SquareRead, location: string, participant: string, sessionId: string, sessionIdNow?: string): boolean;
|
|
40
33
|
export declare function publishWakeRoute(artifact: import('./ports.js').SquareArtifactPort, route: Omit<WakeRoute, 'updatedAt'> & RouteEpoch, opts?: {
|
|
41
34
|
at?: number;
|
|
42
35
|
requireCurrentSession?: boolean;
|
package/dist/routes.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { nameKey } from './model.js';
|
|
2
|
-
import { isCurrentlyJoined } from './runtime.js';
|
|
3
2
|
import { canonicalPath, canonicalPathSync } from './canonical-path.js';
|
|
4
3
|
export { WAKE_ROUTE_KINDS } from './model.js';
|
|
5
4
|
export const ROUTE_FRESH_MS = 24 * 60 * 60 * 1000;
|
|
@@ -44,89 +43,61 @@ function sameRouteAddress(left, right) {
|
|
|
44
43
|
const rightKeys = Object.keys(right).sort();
|
|
45
44
|
return leftKeys.length === rightKeys.length && leftKeys.every((key, index) => rightKeys[index] === key && left[key] === right[key]);
|
|
46
45
|
}
|
|
47
|
-
export function
|
|
46
|
+
export function applyWakeRoute(facts, route, at = Date.now()) {
|
|
48
47
|
const location = canonicalPathSync(route.location);
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}), { ...route, location, updatedAt: at }];
|
|
48
|
+
const identity = routeIdentityKey(route, location);
|
|
49
|
+
facts.replaceRoutes([...facts.routes().filter((item) => routeIdentityKey(item, canonicalPathSync(item.location)) !== identity),
|
|
50
|
+
{ ...route, location, updatedAt: at, extra: {} }]);
|
|
53
51
|
}
|
|
54
|
-
export function
|
|
52
|
+
export function dropSessionWakeRoutes(facts, location, sessionId, expectedEpoch) {
|
|
55
53
|
const canonical = canonicalPathSync(location);
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
return true;
|
|
59
|
-
if (expectedEpoch === undefined)
|
|
60
|
-
return false;
|
|
61
|
-
return item.epoch !== expectedEpoch;
|
|
62
|
-
});
|
|
54
|
+
facts.replaceRoutes(facts.routes().filter((item) => item.sessionId !== sessionId || canonicalPathSync(item.location) !== canonical
|
|
55
|
+
|| (expectedEpoch !== undefined && item.epoch !== expectedEpoch)));
|
|
63
56
|
}
|
|
64
|
-
/**
|
|
65
|
-
|
|
66
|
-
*
|
|
67
|
-
* A route survives when the ending transaction did not commit `done` and this session
|
|
68
|
-
* still holds the sole live presence for its participant — an in-flight same-session
|
|
69
|
-
* rejoin. `liveParticipants === undefined` means the ledger could not be read, so
|
|
70
|
-
* nothing is dropped on suspicion. An explicit epoch fence additionally keeps routes
|
|
71
|
-
* published by a later generation.
|
|
72
|
-
*/
|
|
73
|
-
export function dropEndedSessionWakeRoutesFromState(state, location, sessionId, opts) {
|
|
57
|
+
/** Keep an in-flight same-session rejoin; unknown host evidence is not a reason to retire it. */
|
|
58
|
+
export function dropEndedSessionWakeRoutes(facts, location, sessionId, opts) {
|
|
74
59
|
const canonical = canonicalPathSync(location);
|
|
75
|
-
|
|
60
|
+
facts.replaceRoutes(facts.routes().filter((item) => {
|
|
76
61
|
if (item.sessionId !== sessionId || canonicalPathSync(item.location) !== canonical)
|
|
77
62
|
return true;
|
|
78
63
|
if (opts.participant !== undefined && nameKey(item.participant) !== nameKey(opts.participant))
|
|
79
64
|
return true;
|
|
80
|
-
if (!opts.force)
|
|
81
|
-
|
|
82
|
-
// holds the sole live presence — an in-flight same-session rejoin. A no-longer-joined
|
|
83
|
-
// participant is retired even when a stale presence row survives.
|
|
84
|
-
if (isCurrentlyJoined(state.acts, item.participant)
|
|
85
|
-
&& (opts.liveParticipants === undefined || opts.liveParticipants.has(nameKey(item.participant))))
|
|
86
|
-
return true;
|
|
87
|
-
}
|
|
88
|
-
if (opts.expectedEpoch !== undefined && item.epoch !== opts.expectedEpoch)
|
|
65
|
+
if (!opts.force && facts.participant(item.participant)?.joined
|
|
66
|
+
&& (opts.liveParticipants === undefined || opts.liveParticipants.has(nameKey(item.participant))))
|
|
89
67
|
return true;
|
|
90
|
-
return
|
|
91
|
-
});
|
|
68
|
+
return opts.expectedEpoch !== undefined && item.epoch !== opts.expectedEpoch;
|
|
69
|
+
}));
|
|
92
70
|
}
|
|
93
|
-
export function
|
|
71
|
+
export function dropParticipantWakeRoutes(facts, location, participant) {
|
|
94
72
|
const canonical = canonicalPathSync(location);
|
|
95
|
-
|
|
73
|
+
facts.replaceRoutes(facts.routes().filter((item) => !(nameKey(item.participant) === nameKey(participant) && canonicalPathSync(item.location) === canonical)));
|
|
96
74
|
}
|
|
97
|
-
export function sessionCanEndParticipant(
|
|
75
|
+
export function sessionCanEndParticipant(facts, location, participant, sessionId, sessionIdNow) {
|
|
98
76
|
const canonical = canonicalPathSync(location);
|
|
99
|
-
const owned =
|
|
77
|
+
const owned = facts.routes().filter((item) => nameKey(item.participant) === nameKey(participant) && canonicalPathSync(item.location) === canonical);
|
|
100
78
|
if (owned.length === 0)
|
|
101
|
-
return
|
|
102
|
-
return owned.every((item) => item.sessionId === sessionId) && (
|
|
79
|
+
return sessionIdNow === sessionId;
|
|
80
|
+
return owned.every((item) => item.sessionId === sessionId) && (sessionIdNow === undefined || sessionIdNow === sessionId);
|
|
103
81
|
}
|
|
104
82
|
export async function publishWakeRoute(artifact, route, opts = {}) {
|
|
105
83
|
const location = await canonicalPath(route.location);
|
|
106
84
|
const at = opts.at ?? Date.now();
|
|
107
|
-
await artifact.transact((
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const
|
|
114
|
-
const matching = (state.routes ?? []).filter((item) => routeIdentityKey(item, canonicalPathSync(item.location)) === identity);
|
|
85
|
+
await artifact.transact((facts) => {
|
|
86
|
+
const routes = facts.routes();
|
|
87
|
+
if (opts.requireCurrentSession && (!facts.participant(route.participant)?.joined
|
|
88
|
+
|| routes.some((item) => nameKey(item.participant) === nameKey(route.participant) && canonicalPathSync(item.location) === location && item.sessionId !== route.sessionId)))
|
|
89
|
+
return;
|
|
90
|
+
const identity = routeIdentityKey(route, location);
|
|
91
|
+
const matching = routes.filter((item) => routeIdentityKey(item, canonicalPathSync(item.location)) === identity);
|
|
115
92
|
const existing = matching[0];
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
return { state: { ...state, routes: [...(state.routes ?? []).filter((item) => routeIdentityKey(item, canonicalPathSync(item.location)) !== identity), { ...route, location, updatedAt: at }] }, result: undefined };
|
|
93
|
+
if (matching.length === 1 && existing.updatedAt > at - ROUTE_FRESH_MS && existing.kind === route.kind
|
|
94
|
+
&& existing.channel === route.channel && sameRouteAddress(existing.address, route.address) && existing.epoch === route.epoch)
|
|
95
|
+
return;
|
|
96
|
+
applyWakeRoute(facts, { ...route, location }, at);
|
|
121
97
|
});
|
|
122
98
|
}
|
|
123
99
|
export async function retireWakeRouteFromArtifact(artifact, route, opts = {}) {
|
|
124
100
|
const location = await canonicalPath(route.location);
|
|
125
|
-
const target = routeIdentityKey(
|
|
126
|
-
await artifact.transact((
|
|
127
|
-
const itemLocation = canonicalPathSync(item.location);
|
|
128
|
-
if (routeIdentityKey(item, itemLocation) !== target)
|
|
129
|
-
return true;
|
|
130
|
-
return opts.expectedEpoch !== undefined && item.epoch !== opts.expectedEpoch;
|
|
131
|
-
}) }, result: undefined }));
|
|
101
|
+
const target = routeIdentityKey(route, location);
|
|
102
|
+
await artifact.transact((facts) => facts.replaceRoutes(facts.routes().filter((item) => routeIdentityKey(item, canonicalPathSync(item.location)) !== target || (opts.expectedEpoch !== undefined && item.epoch !== opts.expectedEpoch))));
|
|
132
103
|
}
|
package/dist/runtime.d.ts
CHANGED
|
@@ -1,48 +1,21 @@
|
|
|
1
|
-
import { type ActivityId
|
|
2
|
-
import { type StoredAct, type
|
|
1
|
+
import { type ActivityId } from './square-core.js';
|
|
2
|
+
import { type StoredAct, type WatchLease } from './model.js';
|
|
3
|
+
import type { SquareRead } from './ports.js';
|
|
3
4
|
export declare const WATCH_HEARTBEAT_MS: number;
|
|
4
5
|
export declare const SLEEP_MS: number;
|
|
5
6
|
export declare const STALE_MS: number;
|
|
6
7
|
export declare const WATCH_STALE_MS: number;
|
|
7
8
|
export declare const THROTTLE_WINDOW_MS: number;
|
|
8
9
|
export declare const UNREAD_BLOCK_GRACE_MS: number;
|
|
9
|
-
/** A held express waits for the hand to lower, but not forever: past this budget it gives up exactly like --no-wait (draft saved, retry printed). 0 disables waiting. */
|
|
10
10
|
export declare const HELD_WAIT_BUDGET_MS: number;
|
|
11
|
+
export declare const FACT_PAGE_SIZE = 100;
|
|
11
12
|
export declare function nowMs(): number;
|
|
12
|
-
|
|
13
|
-
export declare function
|
|
14
|
-
body: string;
|
|
15
|
-
mentions?: readonly string[];
|
|
16
|
-
reach?: Reach;
|
|
17
|
-
}, mention: string | true): boolean;
|
|
18
|
-
export declare function foldedState(squareState: SquareState): import("./square-core.js").FoldedSquareState;
|
|
19
|
-
export declare function rosterNames(squareState: SquareState): string[];
|
|
20
|
-
export declare function inSquareCount(squareState: SquareState): number;
|
|
21
|
-
export declare function resolveRosterName(squareState: SquareState, name: string): string | undefined;
|
|
22
|
-
export declare function countSays(acts: StoredAct[], name: string): number;
|
|
23
|
-
export declare function sayNumberFor(acts: StoredAct[], target: StoredAct): number;
|
|
24
|
-
/** Shared terminal condition for a participant's watch lifecycle. */
|
|
25
|
-
export declare function watchTerminalStatus(squareState: SquareState, name: string): 'capped' | 'quorum' | undefined;
|
|
26
|
-
export declare function joinedNames(acts: StoredAct[]): Set<string>;
|
|
27
|
-
export declare function isCurrentlyJoined(acts: StoredAct[], name: string): boolean;
|
|
28
|
-
/** Stable index of the recipient's most recent join act, if any. */
|
|
29
|
-
export declare function lastJoinIndex(acts: StoredAct[], name: string): number | undefined;
|
|
30
|
-
export declare function actStableIndex(act: StoredAct): number;
|
|
13
|
+
export declare function rosterNames(facts: SquareRead): string[];
|
|
14
|
+
export declare function inSquareCount(facts: SquareRead): number;
|
|
31
15
|
export declare function actId(actOrIndex: StoredAct | number): ActivityId;
|
|
32
|
-
export declare function
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
export declare function
|
|
37
|
-
export declare function
|
|
38
|
-
/**
|
|
39
|
-
* The last public act the participant was verifiably present for, or -1 when there is no evidence.
|
|
40
|
-
*
|
|
41
|
-
* "Was here" is a claim of presence, so it rides on evidence: the participant's own acts
|
|
42
|
-
* and seen receipts after their latest join. Anything else — in particular acts merely
|
|
43
|
-
* not addressed to them — says nothing about where they actually were.
|
|
44
|
-
*/
|
|
45
|
-
export declare function presenceAnchor(squareState: SquareState, name: string, landed?: LandedAudienceReplay): number;
|
|
46
|
-
export declare function freshWatchLease(squareState: SquareState, name: string, at?: number): WatchLease | undefined;
|
|
47
|
-
export declare function writeWatchLease(squareState: SquareState, name: string, lease: WatchLease): void;
|
|
48
|
-
export declare function removeWatchLease(squareState: SquareState, name: string, leaseId: string): boolean;
|
|
16
|
+
export declare function watchTerminalStatus(facts: SquareRead, name: string): 'capped' | 'quorum' | undefined;
|
|
17
|
+
/** A continuous prefix stops at the first unseen historical audience edge after join. */
|
|
18
|
+
export declare function readCursor(facts: SquareRead, name: string): number;
|
|
19
|
+
/** Presence rides on own activity or exact receipts, not nonaddressed cursor progress. */
|
|
20
|
+
export declare function presenceAnchor(facts: SquareRead, name: string): number;
|
|
21
|
+
export declare function freshWatchLease(facts: SquareRead, name: string, at?: number): WatchLease | undefined;
|
package/dist/runtime.js
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { SquareError,
|
|
1
|
+
import { formatActivityId } from './square-core.js';
|
|
2
|
+
import { SquareError, sameName } from './model.js';
|
|
3
3
|
function parseIntegerEnv(name, fallback) {
|
|
4
4
|
const raw = process.env[name];
|
|
5
5
|
if (raw === undefined)
|
|
6
6
|
return fallback;
|
|
7
7
|
const value = parseInt(raw, 10);
|
|
8
|
-
if (!Number.isFinite(value) || value <= 0)
|
|
8
|
+
if (!Number.isFinite(value) || value <= 0)
|
|
9
9
|
throw new SquareError('invalid_args', `Invalid ${name}: expected a positive integer.`);
|
|
10
|
-
}
|
|
11
10
|
return value;
|
|
12
11
|
}
|
|
13
12
|
export const WATCH_HEARTBEAT_MS = parseIntegerEnv('SQUARE_WATCH_HEARTBEAT_MS', 60000);
|
|
@@ -16,177 +15,47 @@ export const STALE_MS = parseIntegerEnv('SQUARE_WATCH_QUIET_MS', 300000);
|
|
|
16
15
|
export const WATCH_STALE_MS = parseIntegerEnv('SQUARE_WATCH_STALE_MS', 180000);
|
|
17
16
|
export const THROTTLE_WINDOW_MS = parseIntegerEnv('SQUARE_THROTTLE_WINDOW_MS', 60000);
|
|
18
17
|
export const UNREAD_BLOCK_GRACE_MS = parseIntegerEnv('SQUARE_UNREAD_BLOCK_GRACE_MS', 90000);
|
|
19
|
-
/** A held express waits for the hand to lower, but not forever: past this budget it gives up exactly like --no-wait (draft saved, retry printed). 0 disables waiting. */
|
|
20
18
|
export const HELD_WAIT_BUDGET_MS = parseIntegerEnv('SQUARE_HELD_WAIT_BUDGET_MS', 10 * 60 * 1000);
|
|
19
|
+
export const FACT_PAGE_SIZE = 100;
|
|
21
20
|
export function nowMs() {
|
|
22
21
|
const override = process.env.SQUARE_NOW_MS;
|
|
23
22
|
if (override === undefined)
|
|
24
23
|
return Date.now();
|
|
25
24
|
const value = parseInt(override, 10);
|
|
26
|
-
if (!Number.isFinite(value))
|
|
25
|
+
if (!Number.isFinite(value))
|
|
27
26
|
throw new SquareError('invalid_args', 'Invalid SQUARE_NOW_MS: expected integer milliseconds.');
|
|
28
|
-
}
|
|
29
27
|
return value;
|
|
30
28
|
}
|
|
31
|
-
|
|
32
|
-
export function
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
38
|
-
export function foldedState(squareState) {
|
|
39
|
-
return fold(squareState.acts);
|
|
40
|
-
}
|
|
41
|
-
export function rosterNames(squareState) {
|
|
42
|
-
return foldedState(squareState).participants.map((participant) => participant.name);
|
|
43
|
-
}
|
|
44
|
-
export function inSquareCount(squareState) {
|
|
45
|
-
return foldedState(squareState).joined.length;
|
|
46
|
-
}
|
|
47
|
-
export function resolveRosterName(squareState, name) {
|
|
48
|
-
return findParticipantName(rosterNames(squareState), name);
|
|
49
|
-
}
|
|
50
|
-
export function countSays(acts, name) {
|
|
51
|
-
return acts.filter((act) => act.kind === 'say' && sameName(act.actor, name)).length;
|
|
52
|
-
}
|
|
53
|
-
export function sayNumberFor(acts, target) {
|
|
54
|
-
if (target.kind !== 'say')
|
|
55
|
-
throw new Error('sayNumberFor only applies to say acts');
|
|
56
|
-
let number = 0;
|
|
57
|
-
for (const act of acts) {
|
|
58
|
-
if (act.kind === 'say' && sameName(act.actor, target.actor))
|
|
59
|
-
number++;
|
|
60
|
-
if (act === target)
|
|
61
|
-
return number;
|
|
62
|
-
if (target.index !== undefined && act.index === target.index)
|
|
63
|
-
return number;
|
|
64
|
-
}
|
|
65
|
-
throw new Error('target say act is not present in act history');
|
|
66
|
-
}
|
|
67
|
-
/** Shared terminal condition for a participant's watch lifecycle. */
|
|
68
|
-
export function watchTerminalStatus(squareState, name) {
|
|
69
|
-
if (squareState.hardCap !== null && countSays(squareState.acts, name) >= squareState.hardCap)
|
|
29
|
+
export function rosterNames(facts) { return facts.roster().map((participant) => participant.name); }
|
|
30
|
+
export function inSquareCount(facts) { return facts.roster().filter((participant) => participant.joined).length; }
|
|
31
|
+
export function actId(actOrIndex) { return formatActivityId(typeof actOrIndex === 'number' ? actOrIndex : actOrIndex.index); }
|
|
32
|
+
export function watchTerminalStatus(facts, name) {
|
|
33
|
+
const cap = facts.header().hardCap;
|
|
34
|
+
if (cap !== null && (facts.participant(name)?.sayCount ?? 0) >= cap)
|
|
70
35
|
return 'capped';
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
last = actStableIndex(act);
|
|
88
|
-
}
|
|
89
|
-
return last;
|
|
90
|
-
}
|
|
91
|
-
export function actStableIndex(act) {
|
|
92
|
-
if (act.index === undefined)
|
|
93
|
-
throw new Error(`act ${act.kind} is missing a stable index`);
|
|
94
|
-
return act.index;
|
|
95
|
-
}
|
|
96
|
-
export function actId(actOrIndex) {
|
|
97
|
-
const index = typeof actOrIndex === 'number' ? actOrIndex : actStableIndex(actOrIndex);
|
|
98
|
-
return formatActivityId(index);
|
|
99
|
-
}
|
|
100
|
-
function canonicalRuntimeName(squareState, name) {
|
|
101
|
-
return resolveRosterName(squareState, name) ?? name;
|
|
102
|
-
}
|
|
103
|
-
export function currentHold(acts) {
|
|
104
|
-
const hold = fold(acts).hold;
|
|
105
|
-
return hold.active ? { active: true, at: hold.at, reason: hold.reason } : { active: false };
|
|
106
|
-
}
|
|
107
|
-
export function publicActs(acts) {
|
|
108
|
-
return acts.filter((act) => act.kind === 'say' || act.kind === 'done');
|
|
109
|
-
}
|
|
110
|
-
export function recordObservation(squareState, name, index, state, at = Date.now()) {
|
|
111
|
-
if (!Number.isSafeInteger(index) || index < 0 || !Number.isFinite(at))
|
|
112
|
-
return false;
|
|
113
|
-
const key = canonicalRuntimeName(squareState, name);
|
|
114
|
-
const id = formatActivityId(index);
|
|
115
|
-
const current = squareState.runtime.observations[key]?.[id];
|
|
116
|
-
if (current?.state === 'seen' || (current?.state === state && current.at >= at))
|
|
117
|
-
return false;
|
|
118
|
-
const next = {
|
|
119
|
-
state: 'seen',
|
|
120
|
-
at: Math.max(current?.at ?? -Infinity, at),
|
|
121
|
-
};
|
|
122
|
-
((squareState.runtime.observations ??= {})[key] ??= {})[id] = next;
|
|
123
|
-
return true;
|
|
124
|
-
}
|
|
125
|
-
export function readCursor(squareState, name, landed = replayLandedAudiences(squareState.acts)) {
|
|
126
|
-
const recipient = landed.resolveParticipant(name) ?? name;
|
|
127
|
-
const boundary = landed.lastJoinIndex(recipient) ?? -1;
|
|
128
|
-
let cursor = boundary;
|
|
129
|
-
for (const act of squareState.acts) {
|
|
130
|
-
if (act.index <= boundary || act.kind === 'read' || act.actor === undefined)
|
|
131
|
-
continue;
|
|
132
|
-
if (sameName(act.actor, recipient)) {
|
|
133
|
-
cursor = act.index;
|
|
134
|
-
continue;
|
|
135
|
-
}
|
|
136
|
-
if (!landed.includes(act, recipient)) {
|
|
137
|
-
cursor = act.index;
|
|
138
|
-
continue;
|
|
139
|
-
}
|
|
140
|
-
if (squareState.runtime.observations?.[recipient]?.[formatActivityId(act.index)]?.state !== 'seen')
|
|
141
|
-
break;
|
|
142
|
-
cursor = act.index;
|
|
143
|
-
}
|
|
144
|
-
return cursor;
|
|
145
|
-
}
|
|
146
|
-
/**
|
|
147
|
-
* The last public act the participant was verifiably present for, or -1 when there is no evidence.
|
|
148
|
-
*
|
|
149
|
-
* "Was here" is a claim of presence, so it rides on evidence: the participant's own acts
|
|
150
|
-
* and seen receipts after their latest join. Anything else — in particular acts merely
|
|
151
|
-
* not addressed to them — says nothing about where they actually were.
|
|
152
|
-
*/
|
|
153
|
-
export function presenceAnchor(squareState, name, landed = replayLandedAudiences(squareState.acts)) {
|
|
154
|
-
const recipient = landed.resolveParticipant(name) ?? name;
|
|
155
|
-
const boundary = landed.lastJoinIndex(recipient) ?? -1;
|
|
36
|
+
const peers = facts.roster().filter((participant) => !sameName(participant.name, name));
|
|
37
|
+
return peers.length > 0 && peers.every((peer) => peer.done) ? 'quorum' : undefined;
|
|
38
|
+
}
|
|
39
|
+
/** A continuous prefix stops at the first unseen historical audience edge after join. */
|
|
40
|
+
export function readCursor(facts, name) {
|
|
41
|
+
const boundary = facts.participant(name)?.lastJoinIndex ?? -1;
|
|
42
|
+
const block = facts.directed(name, { afterIndex: boundary, unseenOnly: true }, { edge: 'oldest', limit: 1 }).items[0]?.index;
|
|
43
|
+
const last = facts.activityHeads({
|
|
44
|
+
afterIndex: boundary, ...(block === undefined ? {} : { beforeIndex: block }), actorRequired: true,
|
|
45
|
+
kinds: ['join', 'done', 'say', 'hold', 'resume', 'listen', 'ignore'],
|
|
46
|
+
}, { edge: 'newest', limit: 1 }).items[0]?.index;
|
|
47
|
+
return Math.max(boundary, last ?? boundary);
|
|
48
|
+
}
|
|
49
|
+
/** Presence rides on own activity or exact receipts, not nonaddressed cursor progress. */
|
|
50
|
+
export function presenceAnchor(facts, name) {
|
|
51
|
+
const boundary = facts.participant(name)?.lastJoinIndex ?? -1;
|
|
156
52
|
if (boundary < 0)
|
|
157
53
|
return -1;
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
continue;
|
|
162
|
-
if (sameName(act.actor, recipient)
|
|
163
|
-
|| squareState.runtime.observations?.[recipient]?.[formatActivityId(act.index)]?.state === 'seen') {
|
|
164
|
-
evidence = act.index;
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
for (let i = squareState.acts.length - 1; i >= 0; i--) {
|
|
168
|
-
const event = squareState.acts[i];
|
|
169
|
-
if (event.index <= boundary || event.index > evidence)
|
|
170
|
-
continue;
|
|
171
|
-
if (event.kind === 'say' || event.kind === 'done')
|
|
172
|
-
return event.index;
|
|
173
|
-
}
|
|
174
|
-
return -1;
|
|
175
|
-
}
|
|
176
|
-
export function freshWatchLease(squareState, name, at = Date.now()) {
|
|
177
|
-
const key = canonicalRuntimeName(squareState, name);
|
|
178
|
-
const lease = squareState.runtime.leases[key];
|
|
179
|
-
if (lease === undefined || lease.expiresAt <= at || at - lease.heartbeatAt > WATCH_STALE_MS)
|
|
180
|
-
return undefined;
|
|
181
|
-
return lease;
|
|
182
|
-
}
|
|
183
|
-
export function writeWatchLease(squareState, name, lease) {
|
|
184
|
-
squareState.runtime.leases[canonicalRuntimeName(squareState, name)] = lease;
|
|
54
|
+
const own = facts.activityHeads({ afterIndex: boundary, actors: [name] }, { edge: 'newest', limit: 1 }).items[0]?.index;
|
|
55
|
+
const evidence = Math.max(boundary, own ?? boundary, facts.receiptFacts(name, boundary).latestActorActivityIndex ?? boundary);
|
|
56
|
+
return facts.activityHeads({ afterIndex: boundary, throughIndex: evidence, kinds: ['say', 'done'] }, { edge: 'newest', limit: 1 }).items[0]?.index ?? -1;
|
|
185
57
|
}
|
|
186
|
-
export function
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
return false;
|
|
190
|
-
delete squareState.runtime.leases[key];
|
|
191
|
-
return true;
|
|
58
|
+
export function freshWatchLease(facts, name, at = Date.now()) {
|
|
59
|
+
const lease = facts.lease(name);
|
|
60
|
+
return lease === undefined || lease.expiresAt <= at || at - lease.heartbeatAt > WATCH_STALE_MS ? undefined : lease;
|
|
192
61
|
}
|