@astrosheep/square 0.3.72 → 0.3.74
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 +0 -3
- package/dist/artifact.js +5 -59
- package/dist/boundary-presentation.d.ts +8 -1
- package/dist/boundary-presentation.js +8 -1
- package/dist/catch-decisions.d.ts +7 -1
- package/dist/catch-decisions.js +21 -8
- package/dist/claude-delivery.d.ts +1 -1
- package/dist/claude-delivery.js +7 -3
- package/dist/cli/context.d.ts +1 -1
- package/dist/cli/ledger-command.d.ts +12 -0
- package/dist/cli/ledger-command.js +68 -0
- package/dist/cli/registry.js +2 -0
- package/dist/help.js +7 -2
- package/dist/host-ledger-file-adapter.d.ts +4 -10
- package/dist/host-ledger-file-adapter.js +238 -198
- package/dist/host-ledger-migration.d.ts +26 -0
- package/dist/host-ledger-migration.js +227 -0
- package/dist/host-ledger-sqlite.d.ts +62 -0
- package/dist/host-ledger-sqlite.js +341 -0
- package/dist/host-ledger.d.ts +13 -4
- package/dist/inbox.d.ts +2 -0
- package/dist/inbox.js +64 -22
- package/dist/ledger-maintenance.d.ts +30 -0
- package/dist/ledger-maintenance.js +110 -0
- package/dist/mcp.d.ts +2 -1
- package/dist/mcp.js +51 -26
- package/dist/model.js +5 -2
- package/dist/open-square.d.ts +12 -2
- package/dist/open-square.js +42 -4
- package/dist/participant-host.js +6 -2
- package/dist/ports.d.ts +17 -1
- package/dist/runtime.d.ts +2 -1
- package/dist/runtime.js +12 -6
- package/dist/sqlite-transaction.d.ts +5 -0
- package/dist/sqlite-transaction.js +72 -0
- package/dist/square-actions.d.ts +3 -1
- package/dist/square-actions.js +26 -13
- package/dist/square-application.d.ts +4 -0
- package/dist/square-application.js +41 -27
- package/dist/square-facade.d.ts +26 -22
- package/dist/square-migration.js +2 -1
- package/dist/square-sqlite.d.ts +2 -2
- package/dist/square-sqlite.js +15 -4
- package/dist/views.d.ts +3 -1
- package/dist/views.js +37 -29
- package/guides/application-demand-optimization.md +50 -0
- package/guides/catch-cold-path-optimization.md +56 -0
- package/guides/catch-contention-performance.md +157 -0
- package/guides/catch-lock-investigation.md +81 -0
- package/guides/change-waits.md +1 -1
- package/guides/host-ledger.md +53 -6
- package/guides/indexed-host-ledger-performance.md +39 -0
- package/guides/mcp-connection-lifetime.md +22 -0
- package/guides/pi-boundary-pending-reuse.md +13 -0
- package/package.json +1 -1
- package/plugins/claude/.claude-plugin/plugin.json +1 -1
- package/plugins/codex/.codex-plugin/plugin.json +1 -1
- package/plugins/paseo/client/square-message.tsx +35 -19
- package/plugins/pi/square-pi.js +4 -8
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { setTimeout as sleep } from 'node:timers/promises';
|
|
2
|
+
function abort(signal) {
|
|
3
|
+
if (signal?.aborted)
|
|
4
|
+
throw signal.reason ?? new Error('SQLite operation aborted');
|
|
5
|
+
}
|
|
6
|
+
function isBusy(error) {
|
|
7
|
+
return error instanceof Error && /database is locked|database is busy|SQLITE_BUSY/i.test(error.message);
|
|
8
|
+
}
|
|
9
|
+
/** Yield rather than blocking the process while another connection owns SQLite. */
|
|
10
|
+
export async function retrySqliteBusy(operation, signal, retryMs = 25) {
|
|
11
|
+
for (;;) {
|
|
12
|
+
abort(signal);
|
|
13
|
+
try {
|
|
14
|
+
return operation();
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
if (!isBusy(error))
|
|
18
|
+
throw error;
|
|
19
|
+
try {
|
|
20
|
+
await sleep(typeof retryMs === 'function' ? retryMs() : retryMs, undefined, { signal });
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
abort(signal);
|
|
24
|
+
throw error;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/** Acquisition may retry; COMMIT retries retain the callback's result and transaction. */
|
|
30
|
+
export async function inSqliteTransaction(db, write, validate, change, signal) {
|
|
31
|
+
let begun = false, backoffMs = 25;
|
|
32
|
+
// Spread writer contenders out without delaying a writer that already owns the lock.
|
|
33
|
+
const writerDelay = () => {
|
|
34
|
+
const delay = 1 + Math.floor(Math.random() * backoffMs);
|
|
35
|
+
backoffMs = Math.min(100, backoffMs * 2);
|
|
36
|
+
return delay;
|
|
37
|
+
};
|
|
38
|
+
try {
|
|
39
|
+
const validated = await retrySqliteBusy(() => {
|
|
40
|
+
db.exec(write ? 'BEGIN IMMEDIATE' : 'BEGIN');
|
|
41
|
+
begun = true;
|
|
42
|
+
try {
|
|
43
|
+
return validate();
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
db.exec('ROLLBACK');
|
|
47
|
+
begun = false;
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
}, signal, write ? writerDelay : 25);
|
|
51
|
+
abort(signal);
|
|
52
|
+
const result = change(validated);
|
|
53
|
+
if (result !== null && (typeof result === 'object' || typeof result === 'function') && typeof result.then === 'function') {
|
|
54
|
+
void Promise.resolve(result).catch(() => undefined);
|
|
55
|
+
throw new TypeError('SQLite transaction callbacks must be synchronous.');
|
|
56
|
+
}
|
|
57
|
+
// A busy commit still owns the writer lock. Retry promptly once readers leave;
|
|
58
|
+
// acquisition contenders keep the longer backoff above.
|
|
59
|
+
await retrySqliteBusy(() => db.exec('COMMIT'), signal, 1);
|
|
60
|
+
begun = false;
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
if (begun) {
|
|
65
|
+
try {
|
|
66
|
+
db.exec('ROLLBACK');
|
|
67
|
+
}
|
|
68
|
+
catch { /* retain original failure */ }
|
|
69
|
+
}
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
72
|
+
}
|
package/dist/square-actions.d.ts
CHANGED
|
@@ -8,7 +8,9 @@ export interface OperationContext extends HostContext {
|
|
|
8
8
|
export type { OwnershipFenceOptions };
|
|
9
9
|
export declare function catchUp(square: OperationContext, name: string, options?: CatchOptions, control?: OperationControl): Promise<CatchSelection>;
|
|
10
10
|
export declare function catchUp(square: OperationContext, name: string, options: CatchOptions, control: OperationControl | undefined, leaseId: string): Promise<CatchSelection | null>;
|
|
11
|
-
export declare function join(square: OperationContext, name: string, control?: OperationControl
|
|
11
|
+
export declare function join(square: OperationContext, name: string, control?: OperationControl, admission?: {
|
|
12
|
+
readonly requireKnown?: boolean;
|
|
13
|
+
}): Promise<{
|
|
12
14
|
readonly name: string;
|
|
13
15
|
readonly activity: Activity | null;
|
|
14
16
|
readonly participantCount: number;
|
package/dist/square-actions.js
CHANGED
|
@@ -2,7 +2,7 @@ import { coreDone, coreHold, coreIgnore, coreListen, coreListening, coreResume,
|
|
|
2
2
|
import { isSquareError, nameKey, SquareError, validateName } from './model.js';
|
|
3
3
|
import { participantIdentity } from './participant-identity.js';
|
|
4
4
|
import { deliverPending } from './delivery-operations.js';
|
|
5
|
-
import { decideCatch } from './catch-decisions.js';
|
|
5
|
+
import { decideCatch, observeCatchSelection, selectCatchPage } from './catch-decisions.js';
|
|
6
6
|
import { inSquareCount } from './runtime.js';
|
|
7
7
|
import { claimSessionParticipant, claimSessionTakeover, presenceEpoch, releaseSessionParticipantClaim, readParticipantOwner } from './registry.js';
|
|
8
8
|
import { assertLiveOwner, ensureLocalPresence, identityRouteDraft, processIdentity, publishCommittedIdentity, retireIdentityRoute } from './participant-host.js';
|
|
@@ -14,25 +14,38 @@ export async function catchUp(square, name, options = {}, control, leaseId) {
|
|
|
14
14
|
throwIfAborted(control);
|
|
15
15
|
if (!await assertLiveOwner(square, name))
|
|
16
16
|
throw new SquareError('already_joined', `${participantIdentity(name)} already stands here — another session holds the name`);
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
return
|
|
22
|
-
|
|
23
|
-
}
|
|
17
|
+
let selected;
|
|
18
|
+
if (leaseId === undefined && options.id === undefined) {
|
|
19
|
+
selected = (await square.artifact.read((facts) => {
|
|
20
|
+
const page = selectCatchPage(facts, name, options);
|
|
21
|
+
return page.indexes.length === 0 ? observeCatchSelection(facts, page.participant, [], page.filter, square.clock()) : undefined;
|
|
22
|
+
}, control?.signal)).value;
|
|
23
|
+
}
|
|
24
|
+
if (selected === undefined)
|
|
25
|
+
selected = (await square.artifact.transact((facts) => {
|
|
26
|
+
// Replacement and selection share the receipt transaction: an old waiter cannot
|
|
27
|
+
// consume a later activity between its heartbeat and its selection.
|
|
28
|
+
if (leaseId !== undefined && facts.lease(name)?.leaseId !== leaseId)
|
|
29
|
+
return null;
|
|
30
|
+
return decideCatch(facts, name, options, square.clock());
|
|
31
|
+
}, control?.signal)).value;
|
|
24
32
|
if (selected !== null)
|
|
25
33
|
await publishCommittedIdentity(square, selected.participant);
|
|
26
34
|
return selected;
|
|
27
35
|
}
|
|
28
|
-
export async function join(square, name, control) {
|
|
36
|
+
export async function join(square, name, control, admission = {}) {
|
|
29
37
|
throwIfAborted(control);
|
|
30
38
|
validateName(name);
|
|
31
39
|
const hostLedger = square.hostLedger;
|
|
32
|
-
const preview = (await square.artifact.read((facts) =>
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
40
|
+
const preview = (await square.artifact.read((facts) => {
|
|
41
|
+
// Express may reenter a known participant, but must never create a new name.
|
|
42
|
+
const selected = admission.requireKnown ? resolveKnownName(facts, name) : name;
|
|
43
|
+
return {
|
|
44
|
+
...decideJoin(facts, selected, square.clock()),
|
|
45
|
+
participantCount: facts.roster().filter((p) => p.joined || p.done).length,
|
|
46
|
+
};
|
|
47
|
+
}, control?.signal)).value;
|
|
48
|
+
name = preview.joinedName;
|
|
36
49
|
if (preview.joinAct === undefined) {
|
|
37
50
|
if (hostLedger === undefined)
|
|
38
51
|
return { name: preview.joinedName, activity: null, participantCount: preview.participantCount, reconnected: false };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Activity, ExpressResult, HistoryQuery, OperationControl, Participant, ParticipantStatus, SquareApplication, SquareApplicationContext, SquareAtInput, SquareBuildInput, SquareSnapshot } from './square-facade.js';
|
|
2
2
|
export type * from './square-facade.js';
|
|
3
|
+
export { createSquareResourceOwner } from './open-square.js';
|
|
3
4
|
export { formatActivityId, parseActivityId, MAX_IDENTITY_SET_SIZE } from './square-core.js';
|
|
4
5
|
export { CATCH_DEFAULT_LIMIT, CATCH_MAX_LIMIT } from './catch-decisions.js';
|
|
5
6
|
export { HELD_WAIT_BUDGET_MS } from './runtime.js';
|
|
@@ -32,3 +33,6 @@ export declare class Square {
|
|
|
32
33
|
retireEndedSessionRoutes(sessionId: string): Promise<void>;
|
|
33
34
|
}
|
|
34
35
|
export declare function createSquareApplication(context: SquareApplicationContext): SquareApplication;
|
|
36
|
+
export declare function createSquareApplication(context: SquareApplicationContext, demand: {
|
|
37
|
+
readonly presentation: false;
|
|
38
|
+
}): SquareApplication<false>;
|
|
@@ -2,7 +2,7 @@ import path from 'node:path';
|
|
|
2
2
|
import crypto from 'node:crypto';
|
|
3
3
|
import { setTimeout as sleep } from 'node:timers/promises';
|
|
4
4
|
import { SquareError, isSquareError, nameKey, validateName } from './model.js';
|
|
5
|
-
import { validateDoneBody } from './decisions.js';
|
|
5
|
+
import { resolveKnownName, validateDoneBody } from './decisions.js';
|
|
6
6
|
import { HELD_WAIT_BUDGET_MS, WATCH_HEARTBEAT_MS, inSquareCount, presenceAnchor } from './runtime.js';
|
|
7
7
|
import { catchLimit, observeCatchSelection } from './catch-decisions.js';
|
|
8
8
|
import { directedSuffix } from './activity-feed.js';
|
|
@@ -19,6 +19,7 @@ import { createSquare, diagnoseSquareFile } from './artifact.js';
|
|
|
19
19
|
import { migrateSquareFile } from './square-migration.js';
|
|
20
20
|
import { discoverSquares, DEFAULT_LIST_DEPTH, DEFAULT_LIST_LIMIT } from './list.js';
|
|
21
21
|
import { streamSquare } from './stream.js';
|
|
22
|
+
export { createSquareResourceOwner } from './open-square.js';
|
|
22
23
|
export { formatActivityId, parseActivityId, MAX_IDENTITY_SET_SIZE } from './square-core.js';
|
|
23
24
|
export { CATCH_DEFAULT_LIMIT, CATCH_MAX_LIMIT } from './catch-decisions.js';
|
|
24
25
|
export { HELD_WAIT_BUDGET_MS } from './runtime.js';
|
|
@@ -82,8 +83,8 @@ async function joinOwned(square, requested, options = {}, control) {
|
|
|
82
83
|
async function prepareExpression(square, requested, control) {
|
|
83
84
|
try {
|
|
84
85
|
checkControl(control);
|
|
85
|
-
const
|
|
86
|
-
return
|
|
86
|
+
const result = await join(square, requested, control, { requireKnown: true });
|
|
87
|
+
return { participant: result.name, activity: result.activity };
|
|
87
88
|
}
|
|
88
89
|
catch (error) {
|
|
89
90
|
throw failure(error, { operation: 'express', subject: 'caller', participant: requested, expressEffect: 'unsent' });
|
|
@@ -139,12 +140,12 @@ async function landExpression(square, name, body, options, transport, control) {
|
|
|
139
140
|
});
|
|
140
141
|
}
|
|
141
142
|
}
|
|
142
|
-
async function expressOwned(square, requested, body, options, transport, control) {
|
|
143
|
+
async function expressOwned(square, requested, body, options, transport, presentation, control) {
|
|
143
144
|
const entry = await prepareExpression(square, requested, control);
|
|
144
145
|
const name = entry.participant;
|
|
145
146
|
let result;
|
|
146
147
|
try {
|
|
147
|
-
const feed = (await square.artifact.read((facts) => {
|
|
148
|
+
const feed = !presentation ? undefined : (await square.artifact.read((facts) => {
|
|
148
149
|
const pendingPublic = directedSuffix(facts, name);
|
|
149
150
|
return {
|
|
150
151
|
pendingPublic,
|
|
@@ -161,7 +162,7 @@ async function expressOwned(square, requested, body, options, transport, control
|
|
|
161
162
|
activity: result.activity, waited: result.waited,
|
|
162
163
|
...(result.replyAuthorNotHere === undefined ? {} : { replyAuthorNotHere: result.replyAuthorNotHere }),
|
|
163
164
|
},
|
|
164
|
-
projection: {
|
|
165
|
+
projection: feed === undefined ? undefined : {
|
|
165
166
|
...fresh, ...feed, participant: name,
|
|
166
167
|
location: square.location, now: square.clock(), automaticDeliveryAvailable: hasAutomaticDeliveryIdentity(square.env),
|
|
167
168
|
...(entry.activity === null ? {} : { reentry: entry.activity }),
|
|
@@ -217,9 +218,11 @@ async function catchOwned(square, requested, options = {}, control) {
|
|
|
217
218
|
if (Object.hasOwn(options, 'replace') && idle === 0)
|
|
218
219
|
throw new SquareError('invalid_args', 'Catch replacement requires a positive idle duration');
|
|
219
220
|
catchLimit(options.limit);
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
221
|
+
const from = (await square.artifact.read((facts) => {
|
|
222
|
+
name = resolveKnownName(facts, requested);
|
|
223
|
+
subject = 'filter';
|
|
224
|
+
return options.from?.map((target) => resolveKnownName(facts, target));
|
|
225
|
+
}, control?.signal)).value;
|
|
223
226
|
selection = { ...options, ...(from === undefined ? {} : { from }) };
|
|
224
227
|
subject = 'caller';
|
|
225
228
|
if (idle === 0) {
|
|
@@ -360,12 +363,14 @@ export class Square {
|
|
|
360
363
|
endOwnedSession(name, sessionId, expectedEpoch) { return endOwnedSession(this.square, name, sessionId, expectedEpoch); }
|
|
361
364
|
retireEndedSessionRoutes(sessionId) { return retireEndedSessionRoutes(this.square, sessionId); }
|
|
362
365
|
}
|
|
363
|
-
export function createSquareApplication(context) {
|
|
366
|
+
export function createSquareApplication(context, demand = { presentation: true }) {
|
|
367
|
+
const presentation = demand.presentation;
|
|
364
368
|
const env = { ...context.env };
|
|
365
369
|
const clock = clockFor(env, context.clock);
|
|
366
370
|
const hostLedger = context.hostLedger ?? hostLedgerForEnv(env);
|
|
367
371
|
const openOptions = { env, clock, hostLedger, wakeTransport: context.wakeTransport };
|
|
368
372
|
const getTransport = defaultTransport(openOptions);
|
|
373
|
+
const resources = context.resources;
|
|
369
374
|
function location() {
|
|
370
375
|
const selected = context.squarePath ?? env.SQUARE_LOCATION;
|
|
371
376
|
if (selected === undefined || selected.trim() === '')
|
|
@@ -385,7 +390,9 @@ export function createSquareApplication(context) {
|
|
|
385
390
|
}
|
|
386
391
|
async function scoped(operation, control) {
|
|
387
392
|
checkControl(control);
|
|
388
|
-
const
|
|
393
|
+
const selected = location();
|
|
394
|
+
const artifact = resources?.artifact(selected);
|
|
395
|
+
const square = await openSquare(selected, { ...openOptions, artifact }, control);
|
|
389
396
|
try {
|
|
390
397
|
return await operation(square);
|
|
391
398
|
}
|
|
@@ -416,7 +423,7 @@ export function createSquareApplication(context) {
|
|
|
416
423
|
const result = await (verb === 'listen' ? listen : ignore)(square, name, target, control);
|
|
417
424
|
return {
|
|
418
425
|
data: result,
|
|
419
|
-
projection: {
|
|
426
|
+
projection: !presentation ? undefined : {
|
|
420
427
|
...await contextProjection(square, name), participant: name,
|
|
421
428
|
requestedTarget: target, target: result.activity?.target ?? target,
|
|
422
429
|
},
|
|
@@ -442,6 +449,8 @@ export function createSquareApplication(context) {
|
|
|
442
449
|
else {
|
|
443
450
|
result = await resume(square, name, control);
|
|
444
451
|
}
|
|
452
|
+
if (!presentation)
|
|
453
|
+
return { data: { activity: result.activity }, projection: undefined };
|
|
445
454
|
const view = await eventPresentation(square, result.activity.id);
|
|
446
455
|
return {
|
|
447
456
|
data: { activity: result.activity },
|
|
@@ -461,7 +470,7 @@ export function createSquareApplication(context) {
|
|
|
461
470
|
}, input.markdown, control?.signal);
|
|
462
471
|
return {
|
|
463
472
|
data: { location: squarePath, ...configuration },
|
|
464
|
-
projection: { location: squarePath, standingCount: 0, held: false, now,
|
|
473
|
+
projection: !presentation ? undefined : { location: squarePath, standingCount: 0, held: false, now,
|
|
465
474
|
automaticDeliveryAvailable: hasAutomaticDeliveryIdentity(env) },
|
|
466
475
|
};
|
|
467
476
|
},
|
|
@@ -472,6 +481,8 @@ export function createSquareApplication(context) {
|
|
|
472
481
|
throw new SquareError('invalid_args', 'Join history limit must be between 1 and 100');
|
|
473
482
|
}
|
|
474
483
|
const data = await joinOwned(square, name, options, control);
|
|
484
|
+
if (!presentation)
|
|
485
|
+
return { data, projection: undefined };
|
|
475
486
|
try {
|
|
476
487
|
const view = await entryPresentation(square, data.participant, last);
|
|
477
488
|
return {
|
|
@@ -485,19 +496,19 @@ export function createSquareApplication(context) {
|
|
|
485
496
|
}, control);
|
|
486
497
|
},
|
|
487
498
|
express(body, options = {}, control) {
|
|
488
|
-
return selected('express', (square, name) => expressOwned(square, name, body, options, getTransport, control), control);
|
|
499
|
+
return selected('express', (square, name) => expressOwned(square, name, body, options, getTransport, presentation, control), control);
|
|
489
500
|
},
|
|
490
501
|
catch(options, control) {
|
|
491
502
|
return selected('catch', async (square, name) => {
|
|
492
503
|
const result = await catchOwned(square, name, options, control);
|
|
493
|
-
const presence = await catchPresence(square, control).catch(() => undefined);
|
|
504
|
+
const presence = !presentation ? undefined : await catchPresence(square, control).catch(() => undefined);
|
|
494
505
|
const view = result.selection;
|
|
495
506
|
const stop = result.stop;
|
|
496
507
|
const status = stop === 'already-active' || stop === 'replaced' || stop === 'capped' || stop === 'quorum'
|
|
497
508
|
? stop : undefined;
|
|
498
509
|
return {
|
|
499
510
|
data: { ...caughtResult(result), ...(status === undefined ? {} : { status }) },
|
|
500
|
-
projection: {
|
|
511
|
+
projection: !presentation ? undefined : {
|
|
501
512
|
...observedContext(square, view.standingCount, view.held, view.participant, view.now),
|
|
502
513
|
participant: view.participant, sayNumbers: view.sayNumbers,
|
|
503
514
|
ownActivityCount: view.ownActivityCount, hardCap: view.hardCap, stop,
|
|
@@ -525,12 +536,15 @@ export function createSquareApplication(context) {
|
|
|
525
536
|
const limit = query.limit ?? (query.at?.length ? null : HISTORY_DEFAULT_LIMIT);
|
|
526
537
|
const kinds = query.at?.length || query.contextBefore !== undefined || query.contextAfter !== undefined || query.since !== undefined ? undefined : NON_READ;
|
|
527
538
|
return (await square.artifact.read((facts) => {
|
|
528
|
-
const selected = selectHistory(facts, query, limit, kinds);
|
|
539
|
+
const selected = selectHistory(facts, query, limit, kinds, presentation);
|
|
529
540
|
if (limit === null && selected.totalMatches > HISTORY_MAX_LIMIT) {
|
|
530
541
|
throw failure(new SquareError('invalid_args', `History context exceeds ${HISTORY_MAX_LIMIT} activities`), {
|
|
531
542
|
operation: 'history', historyBounds: { maxLimit: HISTORY_MAX_LIMIT, totalMatches: selected.totalMatches },
|
|
532
543
|
});
|
|
533
544
|
}
|
|
545
|
+
const data = { activities: publicActivities(selected.activities) };
|
|
546
|
+
if (!presentation)
|
|
547
|
+
return { data, projection: undefined };
|
|
534
548
|
const indexes = selected.activities.map((activity) => activity.index);
|
|
535
549
|
const presenceAnchors = {};
|
|
536
550
|
for (const participant of facts.roster()) {
|
|
@@ -539,7 +553,7 @@ export function createSquareApplication(context) {
|
|
|
539
553
|
(presenceAnchors[anchor] ??= []).push(participant.name);
|
|
540
554
|
}
|
|
541
555
|
return {
|
|
542
|
-
data
|
|
556
|
+
data,
|
|
543
557
|
projection: {
|
|
544
558
|
...observedContext(square, inSquareCount(facts), facts.hold().active), ...selected,
|
|
545
559
|
sayNumbers: facts.sayOrdinals(indexes), presenceAnchors,
|
|
@@ -554,7 +568,7 @@ export function createSquareApplication(context) {
|
|
|
554
568
|
return selected('other', async (square, requested) => {
|
|
555
569
|
const name = (await resolveParticipant(square, requested, control)).name;
|
|
556
570
|
const targets = await listening(square, name, control);
|
|
557
|
-
return { data: { listening: targets }, projection: await contextProjection(square, name, control) };
|
|
571
|
+
return { data: { listening: targets }, projection: !presentation ? undefined : await contextProjection(square, name, control) };
|
|
558
572
|
}, control);
|
|
559
573
|
},
|
|
560
574
|
hold: (reason, control) => lifecycle('hold', reason, control),
|
|
@@ -562,15 +576,15 @@ export function createSquareApplication(context) {
|
|
|
562
576
|
done: (body, control) => lifecycle('done', body, control),
|
|
563
577
|
async status(viewer, control) {
|
|
564
578
|
checkControl(control);
|
|
565
|
-
const selectedViewer = viewer ?? await actor().catch(() => '');
|
|
579
|
+
const selectedViewer = !presentation ? '' : viewer ?? await actor().catch(() => '');
|
|
566
580
|
return scoped(async (square) => (await square.artifact.read((facts) => queryStatus(facts, {
|
|
567
581
|
location: square.location, now: square.clock(), automaticDeliveryAvailable: hasAutomaticDeliveryIdentity(square.env),
|
|
568
|
-
}, selectedViewer), control?.signal)).value, control);
|
|
582
|
+
}, selectedViewer, presentation), control?.signal)).value, control);
|
|
569
583
|
},
|
|
570
584
|
participants(control) {
|
|
571
585
|
return scoped(async (square) => (await square.artifact.read((facts) => queryParticipants(facts, {
|
|
572
586
|
location: square.location, now: square.clock(), automaticDeliveryAvailable: hasAutomaticDeliveryIdentity(square.env),
|
|
573
|
-
}), control?.signal)).value, control);
|
|
587
|
+
}, presentation), control?.signal)).value, control);
|
|
574
588
|
},
|
|
575
589
|
async diagnose(control) {
|
|
576
590
|
checkControl(control);
|
|
@@ -578,7 +592,7 @@ export function createSquareApplication(context) {
|
|
|
578
592
|
const data = await diagnoseSquareFile(squarePath);
|
|
579
593
|
return {
|
|
580
594
|
data,
|
|
581
|
-
projection: { location: squarePath, now: clock(),
|
|
595
|
+
projection: !presentation ? undefined : { location: squarePath, now: clock(),
|
|
582
596
|
...(data.standingCount === undefined ? {} : { standingCount: data.standingCount }) },
|
|
583
597
|
};
|
|
584
598
|
},
|
|
@@ -586,14 +600,14 @@ export function createSquareApplication(context) {
|
|
|
586
600
|
checkControl(control);
|
|
587
601
|
const squarePath = location();
|
|
588
602
|
const data = await migrateSquareFile(squarePath, control?.signal);
|
|
589
|
-
return { data, projection: { location: squarePath, now: clock(), standingCount: data.standing } };
|
|
603
|
+
return { data, projection: !presentation ? undefined : { location: squarePath, now: clock(), standingCount: data.standing } };
|
|
590
604
|
},
|
|
591
605
|
async discover(options, control) {
|
|
592
606
|
checkControl(control);
|
|
593
607
|
const data = await discoverSquares(context.cwd, options, control, openOptions);
|
|
594
608
|
return {
|
|
595
609
|
data,
|
|
596
|
-
projection: { cwd: context.cwd, now: clock(),
|
|
610
|
+
projection: !presentation ? undefined : { cwd: context.cwd, now: clock(),
|
|
597
611
|
depth: options.depth ?? DEFAULT_LIST_DEPTH, limit: options.limit ?? DEFAULT_LIST_LIMIT },
|
|
598
612
|
};
|
|
599
613
|
},
|
|
@@ -633,7 +647,7 @@ export function createSquareApplication(context) {
|
|
|
633
647
|
const compare = (left, right) => left < right ? -1 : left > right ? 1 : 0;
|
|
634
648
|
return compare(a.squarePath, b.squarePath) || compare(nameKey(a.name), nameKey(b.name)) || compare(a.name, b.name);
|
|
635
649
|
});
|
|
636
|
-
return { data: { rows: rows.slice(0, limit), total: rows.length }, projection: { sessionId, now: clock(), limit } };
|
|
650
|
+
return { data: { rows: rows.slice(0, limit), total: rows.length }, projection: !presentation ? undefined : { sessionId, now: clock(), limit } };
|
|
637
651
|
},
|
|
638
652
|
async recovery(control) {
|
|
639
653
|
let projection = { now: clock() };
|
|
@@ -659,7 +673,7 @@ export function createSquareApplication(context) {
|
|
|
659
673
|
}, control);
|
|
660
674
|
}
|
|
661
675
|
catch { /* unavailable recovery observations never replace the original error */ }
|
|
662
|
-
return { data: projection, projection };
|
|
676
|
+
return { data: projection, projection: presentation ? projection : undefined };
|
|
663
677
|
},
|
|
664
678
|
stream(options, control) {
|
|
665
679
|
checkControl(control);
|
package/dist/square-facade.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ActivityId, Perception, Reach } from './square-core.js';
|
|
2
2
|
import type { StoredAct } from './model.js';
|
|
3
|
-
import type { HostLedgerPort, WakeTransportPort, DeliveryResult } from './ports.js';
|
|
3
|
+
import type { HostLedgerPort, WakeTransportPort, DeliveryResult, SquareResourceOwner } from './ports.js';
|
|
4
4
|
import type { DiagnoseResult } from './artifact.js';
|
|
5
5
|
import type { MigrationOutcome } from './square-migration.js';
|
|
6
6
|
export type { ActivityId, Perception, Reach } from './square-core.js';
|
|
@@ -136,10 +136,14 @@ export interface SquareApplicationContext extends Readonly<OpenOptions> {
|
|
|
136
136
|
readonly env: NodeJS.ProcessEnv;
|
|
137
137
|
readonly squarePath?: string;
|
|
138
138
|
readonly participant?: string;
|
|
139
|
+
/** Borrow the owner's connection for the configured square. The caller owns its lifetime;
|
|
140
|
+
* without an owner, each operation opens and closes its own connection. */
|
|
141
|
+
readonly resources?: SquareResourceOwner;
|
|
139
142
|
}
|
|
140
|
-
|
|
143
|
+
/** Data-only callers do not request or pay for presentation work. */
|
|
144
|
+
export interface ApplicationOutcome<Data, Projection, Presentation extends boolean = true> {
|
|
141
145
|
readonly data: Data;
|
|
142
|
-
readonly projection: Projection;
|
|
146
|
+
readonly projection: Presentation extends true ? Projection : undefined;
|
|
143
147
|
}
|
|
144
148
|
export interface ApplicationFailureDetail {
|
|
145
149
|
readonly operation: 'express' | 'join' | 'catch' | 'history' | 'other';
|
|
@@ -444,26 +448,26 @@ export interface StatusProjection extends SquareContextProjection, PresenceClass
|
|
|
444
448
|
}
|
|
445
449
|
export interface ParticipantsProjection extends SquareContextProjection, PresenceClassification {
|
|
446
450
|
}
|
|
447
|
-
export interface SquareApplication {
|
|
451
|
+
export interface SquareApplication<Presentation extends boolean = true> {
|
|
448
452
|
now(): number;
|
|
449
|
-
build(input: BuildApplicationInput, control?: OperationControl): Promise<ApplicationOutcome<BuildApplicationData, SquareContextProjection>>;
|
|
450
|
-
join(options?: JoinApplicationOptions, control?: OperationControl): Promise<ApplicationOutcome<JoinApplicationData, EntryProjection>>;
|
|
451
|
-
express(body: string, options?: ExpressApplicationOptions, control?: OperationControl): Promise<ApplicationOutcome<ExpressApplicationData, ExpressProjection>>;
|
|
452
|
-
catch(options?: CatchApplicationOptions, control?: OperationControl): Promise<ApplicationOutcome<CatchApplicationData, CatchProjection>>;
|
|
453
|
-
history(query?: HistoryApplicationQuery, control?: OperationControl): Promise<ApplicationOutcome<HistoryApplicationData, HistoryProjection>>;
|
|
454
|
-
listen(target: string, control?: OperationControl): Promise<ApplicationOutcome<ListenerChangeData, ListenerProjection>>;
|
|
455
|
-
ignore(target: string, control?: OperationControl): Promise<ApplicationOutcome<ListenerChangeData, ListenerProjection>>;
|
|
456
|
-
listening(control?: OperationControl): Promise<ApplicationOutcome<ListeningData, SquareContextProjection>>;
|
|
457
|
-
hold(reason?: string, control?: OperationControl): Promise<ApplicationOutcome<LifecycleData, LifecycleProjection>>;
|
|
458
|
-
resume(control?: OperationControl): Promise<ApplicationOutcome<LifecycleData, LifecycleProjection>>;
|
|
459
|
-
done(body?: string, control?: OperationControl): Promise<ApplicationOutcome<LifecycleData, LifecycleProjection>>;
|
|
460
|
-
status(viewer?: string, control?: OperationControl): Promise<ApplicationOutcome<StatusApplicationData, StatusProjection>>;
|
|
461
|
-
participants(control?: OperationControl): Promise<ApplicationOutcome<ParticipantsApplicationData, ParticipantsProjection>>;
|
|
462
|
-
diagnose(control?: OperationControl): Promise<ApplicationOutcome<DiagnosisApplicationData, Partial<SquareContextProjection
|
|
463
|
-
migrate(control?: OperationControl): Promise<ApplicationOutcome<MigrationApplicationData, Partial<SquareContextProjection
|
|
464
|
-
discover(options: DiscoveryApplicationOptions, control?: OperationControl): Promise<ApplicationOutcome<DiscoveryApplicationData, DiscoveryProjection>>;
|
|
465
|
-
inbox(sessionId: string, options?: InboxApplicationOptions, control?: OperationControl): Promise<ApplicationOutcome<InboxApplicationData, InboxProjection>>;
|
|
466
|
-
recovery(control?: OperationControl): Promise<ApplicationOutcome<RecoveryApplicationData, RecoveryProjection>>;
|
|
453
|
+
build(input: BuildApplicationInput, control?: OperationControl): Promise<ApplicationOutcome<BuildApplicationData, SquareContextProjection, Presentation>>;
|
|
454
|
+
join(options?: JoinApplicationOptions, control?: OperationControl): Promise<ApplicationOutcome<JoinApplicationData, EntryProjection, Presentation>>;
|
|
455
|
+
express(body: string, options?: ExpressApplicationOptions, control?: OperationControl): Promise<ApplicationOutcome<ExpressApplicationData, ExpressProjection, Presentation>>;
|
|
456
|
+
catch(options?: CatchApplicationOptions, control?: OperationControl): Promise<ApplicationOutcome<CatchApplicationData, CatchProjection, Presentation>>;
|
|
457
|
+
history(query?: HistoryApplicationQuery, control?: OperationControl): Promise<ApplicationOutcome<HistoryApplicationData, HistoryProjection, Presentation>>;
|
|
458
|
+
listen(target: string, control?: OperationControl): Promise<ApplicationOutcome<ListenerChangeData, ListenerProjection, Presentation>>;
|
|
459
|
+
ignore(target: string, control?: OperationControl): Promise<ApplicationOutcome<ListenerChangeData, ListenerProjection, Presentation>>;
|
|
460
|
+
listening(control?: OperationControl): Promise<ApplicationOutcome<ListeningData, SquareContextProjection, Presentation>>;
|
|
461
|
+
hold(reason?: string, control?: OperationControl): Promise<ApplicationOutcome<LifecycleData, LifecycleProjection, Presentation>>;
|
|
462
|
+
resume(control?: OperationControl): Promise<ApplicationOutcome<LifecycleData, LifecycleProjection, Presentation>>;
|
|
463
|
+
done(body?: string, control?: OperationControl): Promise<ApplicationOutcome<LifecycleData, LifecycleProjection, Presentation>>;
|
|
464
|
+
status(viewer?: string, control?: OperationControl): Promise<ApplicationOutcome<StatusApplicationData, StatusProjection, Presentation>>;
|
|
465
|
+
participants(control?: OperationControl): Promise<ApplicationOutcome<ParticipantsApplicationData, ParticipantsProjection, Presentation>>;
|
|
466
|
+
diagnose(control?: OperationControl): Promise<ApplicationOutcome<DiagnosisApplicationData, Partial<SquareContextProjection>, Presentation>>;
|
|
467
|
+
migrate(control?: OperationControl): Promise<ApplicationOutcome<MigrationApplicationData, Partial<SquareContextProjection>, Presentation>>;
|
|
468
|
+
discover(options: DiscoveryApplicationOptions, control?: OperationControl): Promise<ApplicationOutcome<DiscoveryApplicationData, DiscoveryProjection, Presentation>>;
|
|
469
|
+
inbox(sessionId: string, options?: InboxApplicationOptions, control?: OperationControl): Promise<ApplicationOutcome<InboxApplicationData, InboxProjection, Presentation>>;
|
|
470
|
+
recovery(control?: OperationControl): Promise<ApplicationOutcome<RecoveryApplicationData, RecoveryProjection, Presentation>>;
|
|
467
471
|
stream(options: StreamApplicationOptions, control?: OperationControl): AsyncIterable<StreamApplicationRecord>;
|
|
468
472
|
}
|
|
469
473
|
export type JoinApplicationResult = JoinApplicationData;
|
package/dist/square-migration.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { openExistingSquareDatabase } from './artifact.js';
|
|
2
|
+
import { inSqliteTransaction } from './sqlite-transaction.js';
|
|
2
3
|
import { canonicalPath } from './canonical-path.js';
|
|
3
4
|
import { isWakeRouteKind } from './model.js';
|
|
4
5
|
import { parseActivityId } from './square-core.js';
|
package/dist/square-sqlite.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type DatabaseSync } from 'node:sqlite';
|
|
2
2
|
import { SquareError, type ActivityObservation, type HardCap, type StoredAct, type WakeRoute, type WatchLease } from './model.js';
|
|
3
|
-
import type { SquareRead, RouteFact } from './ports.js';
|
|
3
|
+
import type { SquareHeader, SquareRead, RouteFact } from './ports.js';
|
|
4
4
|
import { type LandingWriter } from './square-domain.js';
|
|
5
5
|
/** Explicit import/export DTOs, not a runtime transaction model. */
|
|
6
6
|
export interface SquareRuntimeState {
|
|
@@ -43,7 +43,7 @@ export declare function invalidArtifact(detail: string): SquareError;
|
|
|
43
43
|
export declare function validateStoredActivity(value: unknown): StoredAct;
|
|
44
44
|
export declare function validateWatchLease(value: unknown): WatchLease;
|
|
45
45
|
export declare function validateRoute(value: unknown): RouteFact;
|
|
46
|
-
export declare function validateSqliteHeader(db: DatabaseSync, location?: string):
|
|
46
|
+
export declare function validateSqliteHeader(db: DatabaseSync, location?: string): SquareHeader;
|
|
47
47
|
export interface SqliteDriver extends SquareRead, LandingWriter {
|
|
48
48
|
readonly changed: boolean;
|
|
49
49
|
}
|
package/dist/square-sqlite.js
CHANGED
|
@@ -141,6 +141,7 @@ CREATE TABLE landed_audience (
|
|
|
141
141
|
UNIQUE (act_index, recipient_order)
|
|
142
142
|
) WITHOUT ROWID;
|
|
143
143
|
CREATE INDEX audience_recipient ON landed_audience(recipient_key_json, act_index);`;
|
|
144
|
+
const EXPECTED_SCHEMA_SQL = SCHEMA_SQL.split(';').map(s => s.trim()).filter(Boolean).map(normalizedSql).sort();
|
|
144
145
|
const encode = JSON.stringify;
|
|
145
146
|
const key = (name) => encode(nameKey(name));
|
|
146
147
|
export function invalidArtifact(detail) {
|
|
@@ -330,11 +331,10 @@ export function validateSqliteHeader(db, location = '') {
|
|
|
330
331
|
throw new SquareError('invalid_args', `Square artifact needs migration.\n${migrationRecoveryCommand(location)}`, { reason: 'migration_required' });
|
|
331
332
|
}
|
|
332
333
|
requireFact(identity.user_version === 2, 'unsupported SQLite format version.');
|
|
333
|
-
const expected = SCHEMA_SQL.split(';').map(s => s.trim()).filter(Boolean).map(normalizedSql).sort();
|
|
334
334
|
const actual = db.prepare("SELECT sql FROM sqlite_schema WHERE name NOT GLOB 'sqlite_*' AND sql IS NOT NULL").all().map(r => normalizedSql(string(r.sql))).sort();
|
|
335
|
-
requireFact(actual.length ===
|
|
335
|
+
requireFact(actual.length === EXPECTED_SCHEMA_SQL.length && actual.every((sql, i) => sql === EXPECTED_SCHEMA_SQL[i]), 'unsupported row schema.');
|
|
336
336
|
requireFact(db.prepare('PRAGMA journal_mode').get().journal_mode === (location === ':memory:' ? 'memory' : 'delete'), 'unsupported journal mode.');
|
|
337
|
-
createSqliteDriver(db).header();
|
|
337
|
+
return createSqliteDriver(db).header();
|
|
338
338
|
}
|
|
339
339
|
function activitySelection(filter, alias = 'a') {
|
|
340
340
|
const terms = [], args = [];
|
|
@@ -418,6 +418,10 @@ export function createSqliteDriver(db) {
|
|
|
418
418
|
const r = one('SELECT * FROM participants WHERE name_key_json=?', [key(name)]);
|
|
419
419
|
return r ? participant(r) : undefined;
|
|
420
420
|
},
|
|
421
|
+
participantCounts() {
|
|
422
|
+
const r = one('SELECT COUNT(*) AS known, COALESCE(SUM(joined),0) AS joined, COALESCE(SUM(done),0) AS done FROM participants');
|
|
423
|
+
return immutable({ known: integer(r.known), joined: integer(r.joined), done: integer(r.done) });
|
|
424
|
+
},
|
|
421
425
|
roster: () => immutable(all('SELECT * FROM participants ORDER BY first_index').map(participant)),
|
|
422
426
|
attention: name => immutable(attentionRows('t.listener_key_json=?', [key(name)])),
|
|
423
427
|
incomingAttention: name => immutable(attentionRows('t.target_key_json=?', [key(name)])),
|
|
@@ -468,7 +472,14 @@ export function createSqliteDriver(db) {
|
|
|
468
472
|
sayOrdinals(indexes) {
|
|
469
473
|
indexes.forEach(v => integer(v));
|
|
470
474
|
const result = {};
|
|
471
|
-
|
|
475
|
+
// Only an actor's latest say can use its authoritative lifetime count.
|
|
476
|
+
for (const r of all(`SELECT a.act_index, CASE
|
|
477
|
+
WHEN p.say_count IS NOT NULL AND a.act_index=(SELECT MAX(s.act_index) FROM activities s WHERE s.kind='say' AND s.actor_key_json=a.actor_key_json)
|
|
478
|
+
THEN p.say_count
|
|
479
|
+
ELSE (SELECT COUNT(*) FROM activities s WHERE s.kind='say' AND s.actor_key_json=a.actor_key_json AND s.act_index<=a.act_index)
|
|
480
|
+
END AS n
|
|
481
|
+
FROM activities a LEFT JOIN participants p ON p.name_key_json=a.actor_key_json
|
|
482
|
+
WHERE a.kind='say' AND a.act_index IN (SELECT value FROM json_each(?))`, [encode(indexes)]))
|
|
472
483
|
result[integer(r.act_index)] = integer(r.n, 1);
|
|
473
484
|
return immutable(result);
|
|
474
485
|
},
|
package/dist/views.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ export declare const NON_READ: readonly ["join", "done", "say", "hold", "resume"
|
|
|
11
11
|
export declare function toPublicActivity(stored: StoredAct): Activity;
|
|
12
12
|
export declare function parseRequiredActivityId(id: ActivityId): number;
|
|
13
13
|
/** Compile the public query directly to typed selectors; only search hydrates unselected bodies. */
|
|
14
|
-
export declare function selectHistory(facts: SquareRead, query: HistoryApplicationQuery, limit: number | null, kinds: ActivityFilter['kinds']): {
|
|
14
|
+
export declare function selectHistory(facts: SquareRead, query: HistoryApplicationQuery, limit: number | null, kinds: ActivityFilter['kinds'], presentation?: boolean): {
|
|
15
15
|
continuation?: HistoryContinuation | undefined;
|
|
16
16
|
activities: StoredAct[];
|
|
17
17
|
visibleActivities: StoredAct[];
|
|
@@ -29,8 +29,10 @@ export declare function recoveryObservation(square: OpenSquare, name: string, co
|
|
|
29
29
|
export declare function entryPresentation(square: OpenSquare, name: string, lastN?: number | null, control?: OperationControl): Promise<EntryPresentation>;
|
|
30
30
|
type ParticipantQueryContext = Pick<SquareContextProjection, 'location' | 'now' | 'automaticDeliveryAvailable'>;
|
|
31
31
|
export declare function queryParticipants(facts: SquareRead, context: ParticipantQueryContext): ApplicationOutcome<ParticipantsApplicationData, ParticipantsProjection>;
|
|
32
|
+
export declare function queryParticipants(facts: SquareRead, context: ParticipantQueryContext, presentation: boolean): ApplicationOutcome<ParticipantsApplicationData, ParticipantsProjection, boolean>;
|
|
32
33
|
export declare function listPresentation(square: OpenSquare, control?: OperationControl): Promise<ListPresentation>;
|
|
33
34
|
export declare function queryStatus(facts: SquareRead, context: ParticipantQueryContext, viewer?: string): ApplicationOutcome<StatusApplicationData, StatusProjection>;
|
|
35
|
+
export declare function queryStatus(facts: SquareRead, context: ParticipantQueryContext, viewer: string, presentation: boolean): ApplicationOutcome<StatusApplicationData, StatusProjection, boolean>;
|
|
34
36
|
export declare function eventPresentation(square: OpenSquare, id: ActivityId, control?: OperationControl): Promise<EventPresentation>;
|
|
35
37
|
export declare function catchPresence(square: OpenSquare, control?: OperationControl): Promise<CatchPresence>;
|
|
36
38
|
export declare function streamProjection(square: OpenSquare, cursor: number, recipient?: string, control?: OperationControl): Promise<StreamProjection>;
|