@astrosheep/square 0.3.70 → 0.3.71
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/activity-feed.d.ts +4 -5
- package/dist/activity-feed.js +14 -10
- package/dist/artifact.d.ts +26 -33
- package/dist/artifact.js +346 -317
- package/dist/attention-presentation.js +2 -9
- package/dist/automatic-session.js +6 -6
- package/dist/boundary-presentation.js +3 -3
- package/dist/catch-decisions.d.ts +19 -12
- package/dist/catch-decisions.js +32 -68
- package/dist/claude-delivery.js +7 -7
- package/dist/claude-mod.js +18 -7
- package/dist/cli/catch-presentation.d.ts +7 -0
- package/dist/cli/catch-presentation.js +55 -0
- package/dist/cli/context.d.ts +1 -1
- package/dist/cli/context.js +5 -23
- package/dist/cli/express-presentation.d.ts +10 -0
- package/dist/cli/express-presentation.js +196 -0
- package/dist/cli/maintenance-commands.d.ts +4 -0
- package/dist/cli/maintenance-commands.js +29 -6
- package/dist/cli/observation-commands.d.ts +7 -5
- package/dist/cli/observation-commands.js +275 -262
- package/dist/cli/program.d.ts +1 -1
- package/dist/cli/program.js +43 -56
- package/dist/cli/registry.js +2 -1
- package/dist/cli/square-commands.d.ts +3 -9
- package/dist/cli/square-commands.js +134 -255
- package/dist/codex-queue.js +4 -1
- package/dist/decisions.d.ts +26 -74
- package/dist/decisions.js +190 -335
- package/dist/delivery-health.js +13 -22
- package/dist/delivery-operations.d.ts +0 -11
- package/dist/delivery-operations.js +44 -33
- package/dist/delivery.d.ts +33 -32
- package/dist/delivery.js +70 -81
- package/dist/help.js +6 -1
- package/dist/inbox.d.ts +27 -0
- package/dist/inbox.js +55 -18
- package/dist/index.d.ts +2 -4
- package/dist/index.js +1 -1
- package/dist/list.d.ts +7 -1
- package/dist/list.js +49 -122
- package/dist/mcp.d.ts +1 -1
- package/dist/mcp.js +7 -37
- package/dist/model.d.ts +4 -44
- package/dist/model.js +1 -3
- package/dist/notifications.d.ts +1 -1
- package/dist/notifications.js +13 -7
- package/dist/open-square.d.ts +16 -2
- package/dist/open-square.js +40 -1
- package/dist/perception-projection.d.ts +6 -15
- package/dist/perception-projection.js +6 -26
- package/dist/plugins/opencode/delivery.js +9 -9
- package/dist/plugins/opencode/index.js +3 -3
- package/dist/ports.d.ts +156 -11
- package/dist/presentation-operations.d.ts +1 -1
- package/dist/presentation-operations.js +35 -44
- package/dist/presentation.d.ts +12 -19
- package/dist/presentation.js +33 -54
- package/dist/routes.d.ts +7 -14
- package/dist/routes.js +33 -62
- package/dist/runtime.d.ts +12 -39
- package/dist/runtime.js +33 -164
- package/dist/square-actions.d.ts +11 -21
- package/dist/square-actions.js +171 -271
- package/dist/square-application.d.ts +31 -55
- package/dist/square-application.js +637 -141
- package/dist/square-core.d.ts +3 -94
- package/dist/square-core.js +8 -353
- package/dist/square-domain.d.ts +24 -0
- package/dist/square-domain.js +77 -0
- package/dist/square-facade.d.ts +360 -9
- package/dist/square-migration.d.ts +24 -0
- package/dist/square-migration.js +211 -0
- package/dist/square-projections.d.ts +2 -42
- package/dist/square-projections.js +1 -37
- package/dist/square-sqlite.d.ts +59 -0
- package/dist/square-sqlite.js +702 -0
- package/dist/stream.d.ts +3 -10
- package/dist/stream.js +81 -52
- package/dist/views.d.ts +27 -95
- package/dist/views.js +217 -87
- package/dist/wake-eligibility.d.ts +15 -5
- package/dist/wake-eligibility.js +32 -16
- package/dist/wakes.d.ts +3 -3
- package/dist/wakes.js +31 -27
- package/guides/application-facade-delivery-evidence.md +200 -0
- package/guides/application-facade-design.md +95 -0
- package/guides/change-waits.md +4 -4
- package/guides/migration.md +77 -0
- package/guides/primitive-catch-consolidation.md +85 -0
- package/guides/primitive-history-consolidation.md +76 -0
- package/guides/primitive-status-consolidation.md +87 -0
- package/guides/primitive-storage-consolidation.md +128 -0
- package/guides/row-runtime-delivery-evidence.md +160 -0
- package/guides/row-storage-design.md +567 -0
- package/package.json +1 -1
- package/plugins/claude/.claude-plugin/plugin.json +1 -1
- package/plugins/codex/.codex-plugin/plugin.json +1 -1
- package/plugins/opencode/delivery.ts +11 -11
- package/plugins/opencode/index.ts +3 -3
- package/plugins/paseo/client/square-message.tsx +70 -52
- package/plugins/paseo/shared/message.ts +15 -0
- package/plugins/pi/square-pi.js +3 -0
- package/dist/activity.d.ts +0 -14
- package/dist/activity.js +0 -248
- package/dist/square-file-adapter.d.ts +0 -26
- package/dist/square-file-adapter.js +0 -85
- package/dist/square-state.d.ts +0 -6
- package/dist/square-state.js +0 -138
- package/dist/square-storage.d.ts +0 -14
- package/dist/square-storage.js +0 -188
- package/dist/square-wiring.d.ts +0 -33
- package/dist/square-wiring.js +0 -109
- package/dist/watch.d.ts +0 -8
- package/dist/watch.js +0 -252
package/dist/square-actions.js
CHANGED
|
@@ -3,212 +3,150 @@ 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
5
|
import { decideCatch } from './catch-decisions.js';
|
|
6
|
+
import { inSquareCount } from './runtime.js';
|
|
6
7
|
import { claimSessionParticipant, claimSessionTakeover, presenceEpoch, releaseSessionParticipantClaim, readParticipantOwner } from './registry.js';
|
|
7
8
|
import { assertLiveOwner, ensureLocalPresence, identityRouteDraft, processIdentity, publishIdentityRoute, retireIdentityRoute } from './participant-host.js';
|
|
8
|
-
import {
|
|
9
|
+
import { applyWakeRoute, dropEndedSessionWakeRoutes, dropParticipantWakeRoutes, dropSessionWakeRoutes, sessionCanEndParticipant } from './routes.js';
|
|
9
10
|
import { parseRequiredActivityId, toPublicActivity } from './views.js';
|
|
11
|
+
/** Host enrichment is best effort after an authoritative artifact commit. */
|
|
12
|
+
async function publishCommittedPresence(square, name, epoch) {
|
|
13
|
+
await ensureLocalPresence(square, name, epoch).catch(() => undefined);
|
|
14
|
+
await publishIdentityRoute(square, name, epoch).catch(() => undefined);
|
|
15
|
+
}
|
|
10
16
|
function throwIfAborted(control) { if (control?.signal?.aborted)
|
|
11
17
|
throw control.signal.reason ?? new Error('Operation aborted'); }
|
|
12
|
-
function
|
|
13
|
-
|
|
14
|
-
if (
|
|
15
|
-
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
while (true) {
|
|
27
|
-
if (!await assertLiveOwner(square, name)) {
|
|
28
|
-
throw new SquareError('already_joined', `✕ ${participantIdentity(name)} already stands here — another session holds the name`);
|
|
29
|
-
}
|
|
30
|
-
const attempt = await square.artifact.transact((state, version) => {
|
|
31
|
-
const decision = decideCatch(state, name, options, square.clock(), project);
|
|
32
|
-
return { ...(decision.changed ? { state } : {}), result: { version, decision } };
|
|
33
|
-
}, control?.signal);
|
|
34
|
-
await ensureLocalPresence(square, name);
|
|
35
|
-
await publishIdentityRoute(square, name);
|
|
36
|
-
if (attempt.decision.delivered.length > 0 || idle === 0) {
|
|
37
|
-
return {
|
|
38
|
-
activities: attempt.decision.delivered.map((activity) => exposeCaught(activity, attempt.decision.perceptions.get(activity.index) ?? 'full')),
|
|
39
|
-
consumedThrough: attempt.decision.consumedThrough,
|
|
40
|
-
idleExpired: false,
|
|
41
|
-
remaining: attempt.decision.remaining,
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
const remaining = deadline - Date.now();
|
|
45
|
-
if (remaining <= 0) {
|
|
46
|
-
return { activities: [], consumedThrough: attempt.decision.consumedThrough, idleExpired: true, remaining: 0 };
|
|
47
|
-
}
|
|
48
|
-
// Publication above is a semantic no-op while the route and presence stay unchanged and
|
|
49
|
-
// unexpired, so only a real external change can move the artifact version. Establish the
|
|
50
|
-
// wait baseline after those self-side effects; if the version moved anyway (own refresh or
|
|
51
|
-
// a racing external commit), re-snapshot instead of waiting on a stale baseline.
|
|
52
|
-
const baseline = (await square.artifact.read(control?.signal)).version;
|
|
53
|
-
if (baseline !== attempt.version)
|
|
54
|
-
continue;
|
|
55
|
-
if (!await square.artifact.changed(baseline, remaining, control?.signal)) {
|
|
56
|
-
return { activities: [], consumedThrough: attempt.decision.consumedThrough, idleExpired: true, remaining: 0 };
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
function storeActs(state, acts) {
|
|
61
|
-
const stored = [];
|
|
62
|
-
for (const act of acts) {
|
|
63
|
-
const item = { ...act, index: state.runtime.nextActIndex };
|
|
64
|
-
state.runtime.nextActIndex += 1;
|
|
65
|
-
state.acts.push(item);
|
|
66
|
-
stored.push(item);
|
|
67
|
-
}
|
|
68
|
-
return stored;
|
|
69
|
-
}
|
|
70
|
-
function committedActivity(stored, verb) {
|
|
71
|
-
const activity = stored[0];
|
|
72
|
-
if (activity === undefined)
|
|
73
|
-
throw new Error(`${verb} activity did not commit`);
|
|
74
|
-
return activity;
|
|
18
|
+
export async function catchUp(square, name, options = {}, control, leaseId) {
|
|
19
|
+
throwIfAborted(control);
|
|
20
|
+
if (!await assertLiveOwner(square, name))
|
|
21
|
+
throw new SquareError('already_joined', `${participantIdentity(name)} already stands here — another session holds the name`);
|
|
22
|
+
const selected = (await square.artifact.transact((facts) => {
|
|
23
|
+
// Replacement and selection share the receipt transaction: an old waiter cannot
|
|
24
|
+
// consume a later activity between its heartbeat and its selection.
|
|
25
|
+
if (leaseId !== undefined && facts.lease(name)?.leaseId !== leaseId)
|
|
26
|
+
return null;
|
|
27
|
+
return decideCatch(facts, name, options, square.clock());
|
|
28
|
+
}, control?.signal)).value;
|
|
29
|
+
if (selected !== null)
|
|
30
|
+
await publishCommittedPresence(square, selected.participant);
|
|
31
|
+
return selected;
|
|
75
32
|
}
|
|
76
33
|
export async function join(square, name, control) {
|
|
77
|
-
// Rejected validation must not perform an ownership claim.
|
|
78
34
|
throwIfAborted(control);
|
|
79
35
|
validateName(name);
|
|
80
36
|
const hostLedger = square.hostLedger;
|
|
81
|
-
const preview = await square.artifact.read(
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
// already_joined error without mutating the artifact.
|
|
97
|
-
}
|
|
98
|
-
else {
|
|
99
|
-
return { name: previewDecision.joinedName, activity: null };
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
else {
|
|
103
|
-
return { name: previewDecision.joinedName, activity: null };
|
|
104
|
-
}
|
|
37
|
+
const preview = (await square.artifact.read((facts) => ({
|
|
38
|
+
...decideJoin(facts, name, square.clock()),
|
|
39
|
+
participantCount: facts.roster().filter((p) => p.joined || p.done).length,
|
|
40
|
+
}), control?.signal)).value;
|
|
41
|
+
if (preview.joinAct === undefined) {
|
|
42
|
+
if (hostLedger === undefined)
|
|
43
|
+
return { name: preview.joinedName, activity: null, participantCount: preview.participantCount, reconnected: false };
|
|
44
|
+
const identity = processIdentity(square.env ?? process.env);
|
|
45
|
+
const owner = await readParticipantOwner(square.location, preview.joinedName, hostLedger);
|
|
46
|
+
if (owner?.session === identity.session)
|
|
47
|
+
return {
|
|
48
|
+
name: preview.joinedName, activity: null, participantCount: preview.participantCount,
|
|
49
|
+
reconnected: identity.channel !== 'unknown',
|
|
50
|
+
};
|
|
51
|
+
// Missing or foreign owner must pass CAS, not become a silent reconnect.
|
|
105
52
|
}
|
|
106
53
|
let epoch;
|
|
107
54
|
let ownershipClaim;
|
|
108
55
|
if (hostLedger !== undefined) {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
epoch = claim?.epoch;
|
|
56
|
+
ownershipClaim = await claimSessionParticipant(square.location, name, hostLedger, square.env ?? process.env, control?.signal);
|
|
57
|
+
epoch = ownershipClaim?.epoch;
|
|
112
58
|
}
|
|
113
59
|
let committed;
|
|
114
60
|
try {
|
|
115
|
-
const now = square.clock();
|
|
116
|
-
const route = await identityRouteDraft(square, name);
|
|
61
|
+
const now = square.clock(), route = await identityRouteDraft(square, name);
|
|
117
62
|
throwIfAborted(control);
|
|
118
|
-
committed = await square.artifact.transact((
|
|
119
|
-
const decision = decideJoin(
|
|
120
|
-
if (decision.joinAct
|
|
121
|
-
|
|
63
|
+
committed = (await square.artifact.transact((facts) => {
|
|
64
|
+
const decision = decideJoin(facts, name, now);
|
|
65
|
+
if (decision.joinAct !== undefined) {
|
|
66
|
+
if (square.location !== undefined && square.location !== 'memory')
|
|
67
|
+
dropParticipantWakeRoutes(facts, square.location, decision.joinedName);
|
|
68
|
+
if (route !== undefined)
|
|
69
|
+
applyWakeRoute(facts, route, now);
|
|
122
70
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}, control?.signal);
|
|
71
|
+
const stored = decision.joinAct === undefined ? null : facts.land(decision.joinAct);
|
|
72
|
+
return {
|
|
73
|
+
name: decision.joinedName, stored,
|
|
74
|
+
participantCount: facts.roster().filter((p) => p.joined || p.done).length,
|
|
75
|
+
};
|
|
76
|
+
}, control?.signal)).value;
|
|
129
77
|
}
|
|
130
78
|
catch (error) {
|
|
131
79
|
if (hostLedger !== undefined)
|
|
132
80
|
await releaseSessionParticipantClaim(hostLedger, ownershipClaim).catch(() => undefined);
|
|
133
81
|
throw error;
|
|
134
82
|
}
|
|
135
|
-
await
|
|
136
|
-
|
|
137
|
-
|
|
83
|
+
await publishCommittedPresence(square, committed.name, epoch);
|
|
84
|
+
return {
|
|
85
|
+
name: committed.name, activity: committed.stored === null ? null : toPublicActivity(committed.stored),
|
|
86
|
+
participantCount: committed.participantCount, reconnected: false,
|
|
87
|
+
};
|
|
138
88
|
}
|
|
139
|
-
/**
|
|
89
|
+
/** Takeover holds the host CAS fence across the single done+join artifact transaction. */
|
|
140
90
|
export async function takeover(square, name, _oldSessionIds = [], control) {
|
|
141
|
-
// Rejected validation must not perform an ownership claim.
|
|
142
91
|
throwIfAborted(control);
|
|
143
92
|
validateName(name);
|
|
144
93
|
const hostLedger = square.hostLedger;
|
|
145
94
|
const commitLifecycle = async () => {
|
|
146
95
|
throwIfAborted(control);
|
|
147
96
|
const now = square.clock();
|
|
148
|
-
|
|
149
|
-
const joinedName = resolveKnownName(
|
|
150
|
-
const
|
|
151
|
-
const
|
|
152
|
-
const decision = decideJoin(state, joinedName, now);
|
|
97
|
+
return (await square.artifact.transact((facts) => {
|
|
98
|
+
const joinedName = resolveKnownName(facts, name);
|
|
99
|
+
const storedDone = facts.land(coreDone(facts, joinedName, '', now));
|
|
100
|
+
const decision = decideJoin(facts, joinedName, now);
|
|
153
101
|
if (decision.joinAct === undefined)
|
|
154
102
|
throw new SquareError('already_joined', `${participantIdentity(joinedName)} could not be reclaimed`);
|
|
155
|
-
const storedJoin =
|
|
156
|
-
|
|
157
|
-
return {
|
|
158
|
-
|
|
159
|
-
|
|
103
|
+
const storedJoin = facts.land(decision.joinAct);
|
|
104
|
+
facts.replaceRoutes(facts.routes().filter((route) => nameKey(route.participant) !== nameKey(joinedName)));
|
|
105
|
+
return {
|
|
106
|
+
name: joinedName, stored: [storedDone, storedJoin],
|
|
107
|
+
participantCount: facts.roster().filter((p) => p.joined || p.done).length,
|
|
108
|
+
};
|
|
109
|
+
}, control?.signal)).value;
|
|
160
110
|
};
|
|
161
111
|
if (hostLedger !== undefined) {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
// commit its lifecycle (a name that never joined, or a standing participant that is no
|
|
165
|
-
// longer joined) must not claim or remove presence. The refusals mirror the transaction.
|
|
166
|
-
const { state } = await square.artifact.read(control?.signal);
|
|
167
|
-
const joinedName = resolveKnownName(state, name); // invalid_args when the name never joined
|
|
168
|
-
coreDone(state, joinedName, '', square.clock()); // already_done/not_joined when not standing
|
|
112
|
+
// Gate the claim on authoritative admission, so a refused takeover never claims presence.
|
|
113
|
+
await square.artifact.read((facts) => coreDone(facts, resolveKnownName(facts, name), '', square.clock()), control?.signal);
|
|
169
114
|
const owner = await readParticipantOwner(square.location, name, hostLedger);
|
|
170
115
|
throwIfAborted(control);
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
const outcome = await claimSessionTakeover(square.location, name, hostLedger, env, {
|
|
174
|
-
expectedEpoch: presenceEpoch(owner),
|
|
175
|
-
expectedSession: owner?.session ?? '',
|
|
116
|
+
const outcome = await claimSessionTakeover(square.location, name, hostLedger, square.env ?? process.env, {
|
|
117
|
+
expectedEpoch: presenceEpoch(owner), expectedSession: owner?.session ?? '',
|
|
176
118
|
}, async (claim) => {
|
|
177
119
|
throwIfAborted(control);
|
|
178
120
|
const committed = await commitLifecycle();
|
|
179
|
-
await
|
|
180
|
-
await publishIdentityRoute(square, committed.name, claim.epoch);
|
|
121
|
+
await publishCommittedPresence(square, committed.name, claim.epoch);
|
|
181
122
|
return committed;
|
|
182
123
|
}, control?.signal);
|
|
183
124
|
if (outcome.status === 'busy')
|
|
184
|
-
throw new SquareError('already_joined',
|
|
185
|
-
return { name: outcome.result.name, activities: outcome.result.stored.map(toPublicActivity), epoch: outcome.epoch };
|
|
125
|
+
throw new SquareError('already_joined', `${participantIdentity(name)} already stands here — another session holds the name`);
|
|
126
|
+
return { name: outcome.result.name, activities: outcome.result.stored.map(toPublicActivity), participantCount: outcome.result.participantCount, epoch: outcome.epoch };
|
|
186
127
|
}
|
|
187
128
|
const committed = await commitLifecycle();
|
|
188
|
-
await
|
|
189
|
-
|
|
190
|
-
return { name: committed.name, activities: committed.stored.map(toPublicActivity) };
|
|
129
|
+
await publishCommittedPresence(square, committed.name);
|
|
130
|
+
return { name: committed.name, activities: committed.stored.map(toPublicActivity), participantCount: committed.participantCount };
|
|
191
131
|
}
|
|
192
132
|
export async function implicitJoin(square, name, control) {
|
|
193
133
|
throwIfAborted(control);
|
|
194
|
-
const now = square.clock();
|
|
195
|
-
const route = await identityRouteDraft(square, name);
|
|
134
|
+
const now = square.clock(), route = await identityRouteDraft(square, name);
|
|
196
135
|
throwIfAborted(control);
|
|
197
|
-
const committed = await square.artifact.transact((
|
|
198
|
-
const decision = decideImplicitJoin(
|
|
136
|
+
const committed = (await square.artifact.transact((facts) => {
|
|
137
|
+
const decision = decideImplicitJoin(facts, name, now);
|
|
199
138
|
if (decision.state === 'done') {
|
|
200
139
|
if (square.location !== undefined && square.location !== 'memory')
|
|
201
|
-
|
|
202
|
-
|
|
140
|
+
dropSessionWakeRoutes(facts, square.location, processIdentity(square.env ?? process.env).session);
|
|
141
|
+
}
|
|
142
|
+
else if (decision.joinAct !== undefined) {
|
|
143
|
+
if (square.location !== undefined && square.location !== 'memory')
|
|
144
|
+
dropParticipantWakeRoutes(facts, square.location, decision.joinedName);
|
|
145
|
+
if (route !== undefined)
|
|
146
|
+
applyWakeRoute(facts, { ...route, participant: decision.joinedName }, now);
|
|
203
147
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
if (square.location !== undefined && square.location !== 'memory')
|
|
207
|
-
dropParticipantWakeRoutesFromState(state, square.location, decision.joinedName);
|
|
208
|
-
if (route !== undefined)
|
|
209
|
-
applyWakeRouteToState(state, { ...route, participant: decision.joinedName }, now);
|
|
210
|
-
return { state, result: { name: decision.joinedName, state: decision.state, stored: committedActivity(storeActs(state, [decision.joinAct]), 'join') } };
|
|
211
|
-
}, control?.signal);
|
|
148
|
+
return { name: decision.joinedName, state: decision.state, stored: decision.joinAct === undefined ? null : facts.land(decision.joinAct) };
|
|
149
|
+
}, control?.signal)).value;
|
|
212
150
|
await ensureLocalPresence(square, committed.name);
|
|
213
151
|
if (committed.state === 'done')
|
|
214
152
|
await retireIdentityRoute(square, committed.name);
|
|
@@ -216,8 +154,6 @@ export async function implicitJoin(square, name, control) {
|
|
|
216
154
|
await publishIdentityRoute(square, committed.name);
|
|
217
155
|
return { name: committed.name, state: committed.state, activity: committed.stored === null ? null : toPublicActivity(committed.stored) };
|
|
218
156
|
}
|
|
219
|
-
/** Only admission failures can advertise a safe retry; storage and post-commit
|
|
220
|
-
* failures must not be mistaken for a rejected send merely by their error code. */
|
|
221
157
|
function rejectUnsentActivity(operation) {
|
|
222
158
|
try {
|
|
223
159
|
return operation();
|
|
@@ -229,27 +165,32 @@ function rejectUnsentActivity(operation) {
|
|
|
229
165
|
}
|
|
230
166
|
}
|
|
231
167
|
export async function express(square, name, body, options = {}, control) {
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
throw new SquareError('already_joined', `✕ ${participantIdentity(name)} already stands here — another session holds the name`, { activityUnsent: true });
|
|
236
|
-
}
|
|
168
|
+
throwIfAborted(control);
|
|
169
|
+
if (!await assertLiveOwner(square, name))
|
|
170
|
+
throw new SquareError('already_joined', `${participantIdentity(name)} already stands here — another session holds the name`, { activityUnsent: true });
|
|
237
171
|
throwIfAborted(control);
|
|
238
172
|
const now = square.clock();
|
|
239
|
-
const reply = rejectUnsentActivity(() =>
|
|
240
|
-
|
|
173
|
+
const reply = rejectUnsentActivity(() => {
|
|
174
|
+
try {
|
|
175
|
+
return options.reply === undefined ? undefined : parseRequiredActivityId(options.reply);
|
|
176
|
+
}
|
|
177
|
+
catch (error) {
|
|
178
|
+
if (isSquareError(error))
|
|
179
|
+
error.facts = { ...error.facts, reason: 'invalid_reply', activityId: options.reply };
|
|
180
|
+
throw error;
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
const committed = (await square.artifact.transact((facts) => {
|
|
184
|
+
// Selected-row decoding is storage work, not evidence that admission safely refused.
|
|
185
|
+
const proposed = decideAct(facts, { name, body, force: options.force ?? false, now, mentions: options.mentions,
|
|
186
|
+
...(options.reach === undefined ? {} : { reach: options.reach }), ...(options.noMention === undefined ? {} : { noMention: options.noMention }), ...('requireExplicitReach' in options ? { requireExplicitReach: options.requireExplicitReach === true } : {}), ...(reply === undefined ? {} : { reply }) });
|
|
187
|
+
const holder = proposed.type === 'held' ? facts.hold().actor : undefined;
|
|
241
188
|
const decision = rejectUnsentActivity(() => {
|
|
242
|
-
const decision =
|
|
243
|
-
if (decision.type === 'blocked')
|
|
244
|
-
|
|
245
|
-
throw new SquareError('behind', `${participantIdentity(name)} has pending activity`, { pending });
|
|
246
|
-
}
|
|
189
|
+
const decision = proposed;
|
|
190
|
+
if (decision.type === 'blocked')
|
|
191
|
+
throw new SquareError('behind', `${participantIdentity(name)} has pending activity`, { pending: decision.activitySummaries.reduce((count, summary) => count + summary.count, 0) });
|
|
247
192
|
if (decision.type === 'held') {
|
|
248
|
-
|
|
249
|
-
throw new SquareError('held', 'The square is held', {
|
|
250
|
-
...(holder === undefined ? {} : { holder }),
|
|
251
|
-
...(decision.reason === undefined ? {} : { holdReason: decision.reason }),
|
|
252
|
-
});
|
|
193
|
+
throw new SquareError('held', 'The square is held', { ...(holder === undefined ? {} : { holder }), ...(decision.reason === undefined ? {} : { holdReason: decision.reason }) });
|
|
253
194
|
}
|
|
254
195
|
if (decision.type === 'capped')
|
|
255
196
|
throw new SquareError('capped', `${participantIdentity(name)} reached the activity cap`);
|
|
@@ -259,85 +200,69 @@ export async function express(square, name, body, options = {}, control) {
|
|
|
259
200
|
throw new SquareError('bell_quota', `${participantIdentity(name)} cannot ring the bell yet`, { retryAfterMs: Math.max(1, decision.nextAt - now) });
|
|
260
201
|
return decision;
|
|
261
202
|
});
|
|
262
|
-
const stored =
|
|
263
|
-
return {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
203
|
+
const stored = facts.land(decision.act);
|
|
204
|
+
return {
|
|
205
|
+
stored,
|
|
206
|
+
observation: {
|
|
207
|
+
standingCount: inSquareCount(facts), held: facts.hold().active, now,
|
|
208
|
+
ownActivityCount: facts.participant(name).sayCount, hardCap: facts.header().hardCap,
|
|
209
|
+
},
|
|
210
|
+
...(decision.replyAuthorNotHere === undefined ? {} : { replyAuthorNotHere: decision.replyAuthorNotHere }),
|
|
211
|
+
};
|
|
212
|
+
}, control?.signal)).value;
|
|
213
|
+
await publishCommittedPresence(square, name);
|
|
214
|
+
const delivery = square.wakeTransport !== undefined && square.hostLedger !== undefined && square.location !== undefined && square.location !== 'memory'
|
|
215
|
+
? await deliverPending({ artifact: square.artifact, hostLedger: square.hostLedger, transport: square.wakeTransport, location: square.location, activity: committed.stored.index, now })
|
|
216
|
+
.catch(() => ({ attempted: 0, accepted: 0, failed: 0, unknown: 0, notCapable: 1 }))
|
|
217
|
+
: { attempted: 0, accepted: 0, failed: 0, unknown: 0, notCapable: 1 };
|
|
218
|
+
return {
|
|
219
|
+
activity: toPublicActivity(committed.stored), delivery, observation: committed.observation,
|
|
220
|
+
...(committed.replyAuthorNotHere === undefined ? {} : { replyAuthorNotHere: committed.replyAuthorNotHere }),
|
|
221
|
+
};
|
|
275
222
|
}
|
|
276
223
|
async function landListenerChange(square, verb, actor, target, control) {
|
|
277
224
|
throwIfAborted(control);
|
|
278
225
|
const now = square.clock();
|
|
279
|
-
const stored = await square.artifact.transact((
|
|
280
|
-
const act = verb === 'listen' ? coreListen(
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
return { state, result: committedActivity(storeActs(state, [act]), verb) };
|
|
284
|
-
}, control?.signal);
|
|
226
|
+
const stored = (await square.artifact.transact((facts) => {
|
|
227
|
+
const act = verb === 'listen' ? coreListen(facts, actor, target, now) : coreIgnore(facts, actor, target, now);
|
|
228
|
+
return act === undefined ? null : facts.land(act);
|
|
229
|
+
}, control?.signal)).value;
|
|
285
230
|
return { activity: stored === null ? null : toPublicActivity(stored) };
|
|
286
231
|
}
|
|
287
232
|
export function listen(square, actor, target, control) { return landListenerChange(square, 'listen', actor, target, control); }
|
|
288
233
|
export function ignore(square, actor, target, control) { return landListenerChange(square, 'ignore', actor, target, control); }
|
|
289
|
-
export async function listening(square, actor, control) {
|
|
234
|
+
export async function listening(square, actor, control) { return (await square.artifact.read((facts) => coreListening(facts, actor), control?.signal)).value; }
|
|
290
235
|
async function landCore(square, verb, actor, body = '', fence = {}, control) {
|
|
291
236
|
if (verb === 'done')
|
|
292
237
|
validateDoneBody(body);
|
|
293
|
-
// Done completes ownership: the live-owner validation, the artifact commit, the ended
|
|
294
|
-
// session's route retirement, and presence cleanup share one ownership critical section.
|
|
295
|
-
// A takeover finalizing between validation and commit can never be completed by a stale
|
|
296
|
-
// done, and no post-lock cleanup can remove a replacement owner's fresh rows.
|
|
297
238
|
const hostLedger = verb === 'done' ? square.hostLedger : undefined;
|
|
298
|
-
const
|
|
299
|
-
const session = fenced ? processIdentity(square.env ?? process.env).session : undefined;
|
|
239
|
+
const session = hostLedger === undefined ? undefined : processIdentity(square.env ?? process.env).session;
|
|
300
240
|
const commitAndCleanup = async () => {
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
if (fenced && session !== undefined) {
|
|
306
|
-
ownerRecords = (await hostLedger.listPresence({ location: square.location, participant: actor }))
|
|
307
|
-
.filter((row) => row.session === session);
|
|
308
|
-
}
|
|
241
|
+
// Owner admission, commit and route/presence cleanup share the host claim critical section.
|
|
242
|
+
if (verb === 'done' && !await assertLiveOwner(square, actor, fence.expectedEpoch))
|
|
243
|
+
throw new SquareError('already_done', `${participantIdentity(actor)} already stands here — another session holds the name`);
|
|
244
|
+
const owners = hostLedger === undefined ? [] : (await hostLedger.listPresence({ location: square.location, participant: actor })).filter((row) => row.session === session);
|
|
309
245
|
const now = square.clock();
|
|
310
|
-
const stored = await square.artifact.transact((
|
|
311
|
-
const
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
for (const ownerRecord of ownerRecords) {
|
|
319
|
-
await hostLedger.removePresenceIfUnchanged(ownerRecord).catch((error) => {
|
|
246
|
+
const stored = (await square.artifact.transact((facts) => {
|
|
247
|
+
const result = facts.land(verb === 'done' ? coreDone(facts, actor, body, now) : verb === 'hold' ? coreHold(facts, actor, body, now) : coreResume(facts, actor, now));
|
|
248
|
+
if (session !== undefined)
|
|
249
|
+
dropEndedSessionWakeRoutes(facts, square.location, session, { expectedEpoch: fence.expectedEpoch, force: true, participant: actor });
|
|
250
|
+
return result;
|
|
251
|
+
}, control?.signal)).value;
|
|
252
|
+
for (const row of owners)
|
|
253
|
+
await hostLedger.removePresenceIfUnchanged(row).catch((error) => {
|
|
320
254
|
process.stderr.write(`! host presence cleanup degraded: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
321
255
|
});
|
|
322
|
-
}
|
|
323
256
|
return stored;
|
|
324
257
|
};
|
|
325
|
-
const stored =
|
|
326
|
-
? await hostLedger.withClaimLock(commitAndCleanup, control?.signal)
|
|
327
|
-
: await commitAndCleanup();
|
|
258
|
+
const stored = hostLedger === undefined ? await commitAndCleanup() : await hostLedger.withClaimLock(commitAndCleanup, control?.signal);
|
|
328
259
|
return { activity: toPublicActivity(stored) };
|
|
329
260
|
}
|
|
330
261
|
export function done(square, name, body = '', fence = {}, control) { return landCore(square, 'done', name, body, fence, control); }
|
|
331
262
|
function liveClaimedParticipants(rows, sessionId) {
|
|
332
|
-
const claimed = new Set();
|
|
333
|
-
const
|
|
334
|
-
|
|
335
|
-
const key = nameKey(row.participant);
|
|
336
|
-
if (row.session === sessionId)
|
|
337
|
-
claimed.add(key);
|
|
338
|
-
else
|
|
339
|
-
foreign.add(key);
|
|
340
|
-
}
|
|
263
|
+
const claimed = new Set(), foreign = new Set();
|
|
264
|
+
for (const row of rows)
|
|
265
|
+
(row.session === sessionId ? claimed : foreign).add(nameKey(row.participant));
|
|
341
266
|
for (const key of foreign)
|
|
342
267
|
claimed.delete(key);
|
|
343
268
|
return claimed;
|
|
@@ -350,30 +275,19 @@ async function readLiveClaimedParticipants(hostLedger, location, sessionId, now)
|
|
|
350
275
|
return undefined;
|
|
351
276
|
}
|
|
352
277
|
}
|
|
353
|
-
/**
|
|
354
|
-
* End one session's ownership of a participant inside the ownership claim critical
|
|
355
|
-
* section. The artifact `done` (when this session is still the owner) and retirement of
|
|
356
|
-
* this session's routes share one transaction, and the exact captured presence row is
|
|
357
|
-
* removed only while the lock is still held. A rejoin or takeover therefore lands either
|
|
358
|
-
* before the whole cleanup — and is part of the state being ended — or after it, with
|
|
359
|
-
* fresh presence and route rows that this cleanup can never touch.
|
|
360
|
-
*/
|
|
361
278
|
export async function endOwnedSession(square, name, sessionId, expectedEpoch) {
|
|
362
|
-
const location = square
|
|
363
|
-
const hostLedger = square.hostLedger;
|
|
279
|
+
const { location, hostLedger } = square;
|
|
364
280
|
const run = async () => {
|
|
365
281
|
const now = square.clock();
|
|
366
|
-
let currentSessionId;
|
|
367
|
-
let ownerMatches = expectedEpoch === undefined;
|
|
282
|
+
let currentSessionId, ownerMatches = expectedEpoch === undefined;
|
|
368
283
|
let ownerRecords = [];
|
|
369
284
|
let liveParticipants = new Set();
|
|
370
285
|
if (location !== undefined && location !== 'memory' && hostLedger !== undefined) {
|
|
371
286
|
try {
|
|
372
287
|
const rows = await hostLedger.listPresence({ location, now });
|
|
373
288
|
const bindings = rows.filter((row) => nameKey(row.participant) === nameKey(name));
|
|
374
|
-
currentSessionId = bindings.find((
|
|
375
|
-
|
|
376
|
-
ownerRecords = rows.filter((row) => row.session === sessionId && nameKey(row.participant) === nameKey(name));
|
|
289
|
+
currentSessionId = bindings.find((row) => row.session !== sessionId)?.session ?? bindings.toSorted((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0))[0]?.session;
|
|
290
|
+
ownerRecords = bindings.filter((row) => row.session === sessionId);
|
|
377
291
|
liveParticipants = liveClaimedParticipants(rows, sessionId);
|
|
378
292
|
if (expectedEpoch !== undefined) {
|
|
379
293
|
const owner = await readParticipantOwner(location, name, hostLedger);
|
|
@@ -381,55 +295,41 @@ export async function endOwnedSession(square, name, sessionId, expectedEpoch) {
|
|
|
381
295
|
}
|
|
382
296
|
}
|
|
383
297
|
catch {
|
|
384
|
-
liveParticipants = undefined;
|
|
298
|
+
liveParticipants = undefined;
|
|
385
299
|
}
|
|
386
300
|
}
|
|
387
|
-
const stored = await square.artifact.transact((
|
|
301
|
+
const stored = (await square.artifact.transact((facts) => {
|
|
388
302
|
let committed = null;
|
|
389
|
-
if (location !== undefined && location !== 'memory' && ownerMatches && sessionCanEndParticipant(
|
|
303
|
+
if (location !== undefined && location !== 'memory' && ownerMatches && sessionCanEndParticipant(facts, location, name, sessionId, currentSessionId)) {
|
|
390
304
|
try {
|
|
391
|
-
committed =
|
|
305
|
+
committed = facts.land(coreDone(facts, name, '', now));
|
|
392
306
|
}
|
|
393
307
|
catch (error) {
|
|
394
308
|
if (!isSquareError(error) || (error.code !== 'already_done' && error.code !== 'not_joined'))
|
|
395
309
|
throw error;
|
|
396
310
|
}
|
|
397
311
|
}
|
|
398
|
-
if (location !== undefined && location !== 'memory')
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
await hostLedger.removePresenceIfUnchanged(ownerRecord).catch((error) => {
|
|
312
|
+
if (location !== undefined && location !== 'memory')
|
|
313
|
+
dropEndedSessionWakeRoutes(facts, location, sessionId, { expectedEpoch, liveParticipants, force: committed !== null, participant: name });
|
|
314
|
+
return committed;
|
|
315
|
+
})).value;
|
|
316
|
+
if (stored !== null)
|
|
317
|
+
for (const row of ownerRecords)
|
|
318
|
+
await hostLedger.removePresenceIfUnchanged(row).catch((error) => {
|
|
406
319
|
process.stderr.write(`! host presence cleanup degraded: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
407
320
|
});
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
321
|
return stored;
|
|
411
322
|
};
|
|
412
|
-
const stored = hostLedger
|
|
413
|
-
? await hostLedger.withClaimLock(run)
|
|
414
|
-
: await run();
|
|
323
|
+
const stored = hostLedger === undefined ? await run() : await hostLedger.withClaimLock(run);
|
|
415
324
|
return stored === null ? null : { activity: toPublicActivity(stored) };
|
|
416
325
|
}
|
|
417
|
-
/**
|
|
418
|
-
* Retire an ended session's orphan routes when no participant name is known. The
|
|
419
|
-
* ownership claim lock is held across the ledger read and the artifact transaction, so a
|
|
420
|
-
* rejoin that claims first is seen as a live participant and its route survives.
|
|
421
|
-
*/
|
|
422
326
|
export async function retireEndedSessionRoutes(square, sessionId) {
|
|
423
|
-
const location = square
|
|
424
|
-
const hostLedger = square.hostLedger;
|
|
327
|
+
const { location, hostLedger } = square;
|
|
425
328
|
if (location === undefined || hostLedger === undefined)
|
|
426
329
|
return;
|
|
427
330
|
await hostLedger.withClaimLock(async () => {
|
|
428
331
|
const liveParticipants = await readLiveClaimedParticipants(hostLedger, location, sessionId, square.clock());
|
|
429
|
-
await square.artifact.transact((
|
|
430
|
-
dropEndedSessionWakeRoutesFromState(state, location, sessionId, { liveParticipants, force: false });
|
|
431
|
-
return { state, result: undefined };
|
|
432
|
-
});
|
|
332
|
+
await square.artifact.transact((facts) => { dropEndedSessionWakeRoutes(facts, location, sessionId, { liveParticipants, force: false }); });
|
|
433
333
|
});
|
|
434
334
|
}
|
|
435
335
|
export function hold(square, name, reason = '', control) { return landCore(square, 'hold', name, reason, {}, control); }
|