@astrosheep/square 0.3.41 → 0.3.43
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/claude-plugin/.claude-plugin/plugin.json +1 -1
- package/codex-plugin/.codex-plugin/plugin.json +1 -1
- package/dist/attention-presentation.d.ts +1 -1
- package/dist/attention-presentation.js +1 -1
- package/dist/automatic-session.js +37 -22
- package/dist/boundary-presentation.js +1 -1
- package/dist/codex-boundary-state.js +2 -2
- package/dist/delivery-operations.d.ts +2 -2
- package/dist/delivery-operations.js +144 -45
- package/dist/file-lock.d.ts +5 -1
- package/dist/file-lock.js +16 -91
- package/dist/host-ledger-file-adapter.d.ts +6 -2
- package/dist/host-ledger-file-adapter.js +42 -23
- package/dist/host-ledger.d.ts +13 -3
- package/dist/notifications.js +32 -3
- package/dist/ports.d.ts +5 -2
- package/dist/presentation-operations.js +8 -6
- package/dist/registry.d.ts +40 -2
- package/dist/registry.js +141 -4
- package/dist/routes.d.ts +15 -3
- package/dist/routes.js +72 -12
- package/dist/runtime.d.ts +0 -1
- package/dist/runtime.js +0 -1
- package/dist/square-actions.d.ts +7 -4
- package/dist/square-actions.js +239 -39
- package/dist/square-facade.d.ts +4 -0
- package/dist/square-projections.d.ts +7 -0
- package/dist/square-projections.js +16 -3
- package/dist/square-storage.js +59 -13
- package/dist/square-wiring.d.ts +2 -1
- package/dist/square-wiring.js +2 -1
- package/dist/state-cell.d.ts +4 -1
- package/dist/wake-attempts.js +8 -2
- package/package.json +1 -1
package/dist/registry.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
/** Machine-local participant discovery cache. */
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
|
+
import { withFileLock } from './file-lock.js';
|
|
4
5
|
import { sameName, SquareError } from './model.js';
|
|
5
6
|
import { isCurrentlyJoined } from './runtime.js';
|
|
6
7
|
import { squareAssignedParticipantName as computeSquareAssignedParticipantName } from './participant-identity.js';
|
|
7
8
|
import { createHostLedgerPort } from './host-ledger-file-adapter.js';
|
|
8
9
|
const MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
10
|
+
const PRESENCE_CLAIM_LOCK = { retryMs: 10 };
|
|
9
11
|
const LOCAL_SESSION_SOURCES = [
|
|
10
12
|
{ variable: 'CLAUDE_CODE_SESSION_ID', channel: 'claude-code', child: 'CLAUDE_CODE_CHILD_SESSION' },
|
|
11
13
|
{ variable: 'CODEX_THREAD_ID', channel: 'codex' },
|
|
@@ -19,8 +21,38 @@ export async function canonicalSquarePath(squarePath) { const absolute = path.re
|
|
|
19
21
|
catch {
|
|
20
22
|
return absolute;
|
|
21
23
|
} }
|
|
22
|
-
function
|
|
23
|
-
|
|
24
|
+
function ledgerRoot(env) {
|
|
25
|
+
const root = path.dirname(registryPath(env));
|
|
26
|
+
return {
|
|
27
|
+
userPath: env.SQUARE_HOST_LEDGER_USER ?? (env.SQUARE_REGISTRY ? root : path.join(homedir(), '.square', 'host-ledger')),
|
|
28
|
+
localPath: env.SQUARE_HOST_LEDGER_LOCAL ?? (env.SQUARE_REGISTRY ? root : path.join(process.cwd(), '.square', 'host-ledger')),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function ledger(env, writableScope = 'local') {
|
|
32
|
+
return createHostLedgerPort({ ...ledgerRoot(env), writableScope });
|
|
33
|
+
}
|
|
34
|
+
export function presenceEpoch(record) {
|
|
35
|
+
return typeof record?.epoch === 'number' && Number.isSafeInteger(record.epoch) && record.epoch > 0 ? record.epoch : 0;
|
|
36
|
+
}
|
|
37
|
+
function toBinding(record) {
|
|
38
|
+
return {
|
|
39
|
+
sessionId: record.session,
|
|
40
|
+
name: record.participant,
|
|
41
|
+
squarePath: record.location,
|
|
42
|
+
channel: record.channel,
|
|
43
|
+
child: false,
|
|
44
|
+
...(record.route === undefined ? {} : { route: record.route }),
|
|
45
|
+
updatedAt: record.updatedAt ?? 0,
|
|
46
|
+
epoch: presenceEpoch(record),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function presenceClaimLockPath(env) {
|
|
50
|
+
return path.join(ledgerRoot(env).userPath, 'presence-claim.lock');
|
|
51
|
+
}
|
|
52
|
+
/** Runs `fn` inside the ownership claim critical section, under the same lock as claims and finalize. */
|
|
53
|
+
export async function withOwnershipClaimLock(env, fn) {
|
|
54
|
+
return withFileLock(presenceClaimLockPath(env), PRESENCE_CLAIM_LOCK, fn);
|
|
55
|
+
}
|
|
24
56
|
async function activeBindings(now, env) { return (await ledger(env).listPresence({ now, scopes: ['user', 'local'] })).map(toBinding).sort((a, b) => b.updatedAt - a.updatedAt); }
|
|
25
57
|
async function writePresence(sessionId, name, squarePath, options, done, scope = 'local') { if (!sessionId || !name || !squarePath)
|
|
26
58
|
return; const env = options.env ?? process.env; const channel = options.channel ?? 'unknown'; const port = ledger(env, scope); const location = await canonicalSquarePath(squarePath); if (done)
|
|
@@ -45,17 +77,122 @@ export async function localParticipantOwner(squarePath, name, env = process.env,
|
|
|
45
77
|
return undefined; return (await lookupParticipant(squarePath, name, now, env)).find((binding) => sessionIds.has(binding.sessionId))?.sessionId; }
|
|
46
78
|
export async function localParticipantName(squarePath, env = process.env) { const canonicalPath = await canonicalSquarePath(squarePath); const names = new Set((await Promise.all(localSessionIdentities(env).map(async (identity) => (await lookupSession(identity.sessionId, Date.now(), env)).filter((item) => item.squarePath === canonicalPath).map((item) => item.name)))).flat()); return names.size === 1 ? [...names][0] : undefined; }
|
|
47
79
|
export function squareAssignedParticipantName(env = process.env) { return computeSquareAssignedParticipantName(env); }
|
|
80
|
+
export async function readParticipantOwner(squarePath, name, env = process.env) {
|
|
81
|
+
const bindings = await lookupParticipant(squarePath, name, Date.now(), env);
|
|
82
|
+
return bindings.sort((left, right) => right.epoch - left.epoch || right.updatedAt - left.updatedAt)[0];
|
|
83
|
+
}
|
|
84
|
+
export async function sessionOwnsParticipant(squarePath, name, sessionId, env = process.env, expectedEpoch) {
|
|
85
|
+
const owner = await readParticipantOwner(squarePath, name, env);
|
|
86
|
+
if (owner === undefined || owner.sessionId !== sessionId)
|
|
87
|
+
return false;
|
|
88
|
+
return expectedEpoch === undefined || owner.epoch === expectedEpoch;
|
|
89
|
+
}
|
|
48
90
|
export async function claimSessionParticipant(squarePath, name, env = process.env) {
|
|
49
91
|
const identity = localSessionIdentities(env)[0];
|
|
50
92
|
if (identity === undefined)
|
|
51
93
|
return undefined;
|
|
52
94
|
const location = await canonicalSquarePath(squarePath);
|
|
53
|
-
const result = await ledger(env).claimPresence({
|
|
95
|
+
const result = await ledger(env).claimPresence({
|
|
96
|
+
location,
|
|
97
|
+
participant: name,
|
|
98
|
+
session: identity.sessionId,
|
|
99
|
+
channel: identity.channel,
|
|
100
|
+
updatedAt: Date.now(),
|
|
101
|
+
epoch: 1,
|
|
102
|
+
}, 'local');
|
|
54
103
|
if (result.status === 'busy')
|
|
55
104
|
throw new SquareError('already_joined', `${name} is already bound to another session`);
|
|
56
105
|
if (result.status === 'degraded')
|
|
57
106
|
throw result.error;
|
|
58
|
-
|
|
107
|
+
const epoch = presenceEpoch(result.record) || (result.status === 'acquired' ? 1 : 0);
|
|
108
|
+
return { status: result.status, sessionId: identity.sessionId, epoch };
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Fenced takeover: the ownership claim and the lifecycle run as one critical section under the
|
|
112
|
+
* presence-claim lock, so no second takeover can interleave between claim and commit. The claim
|
|
113
|
+
* persists nothing until the lifecycle commits: standing owner rows stay authoritative through
|
|
114
|
+
* the lifecycle (a self-takeover can therefore never clobber its own standing row). On success
|
|
115
|
+
* the standing rows are replaced by the new owner's row at the claim epoch; on refusal, rows
|
|
116
|
+
* carrying the claim token (session + channel + claim epoch) are withdrawn and the captured
|
|
117
|
+
* standing rows are restored exactly.
|
|
118
|
+
*/
|
|
119
|
+
export async function claimSessionTakeover(squarePath, name, env = process.env, opts = {}, lifecycle) {
|
|
120
|
+
const identity = localSessionIdentities(env)[0];
|
|
121
|
+
if (identity === undefined)
|
|
122
|
+
throw new SquareError('invalid_args', 'No local session identity for takeover');
|
|
123
|
+
const location = await canonicalSquarePath(squarePath);
|
|
124
|
+
return withFileLock(presenceClaimLockPath(env), PRESENCE_CLAIM_LOCK, async () => {
|
|
125
|
+
const port = ledger(env);
|
|
126
|
+
const standing = {
|
|
127
|
+
user: await port.listPresence({ location, participant: name, scopes: ['user'] }),
|
|
128
|
+
local: await port.listPresence({ location, participant: name, scopes: ['local'] }),
|
|
129
|
+
};
|
|
130
|
+
const owner = [...standing.user, ...standing.local]
|
|
131
|
+
.map((row) => toBinding(row))
|
|
132
|
+
.sort((left, right) => right.epoch - left.epoch || right.updatedAt - left.updatedAt)[0];
|
|
133
|
+
const currentEpoch = owner?.epoch ?? 0;
|
|
134
|
+
const epochMatches = opts.expectedEpoch === undefined || currentEpoch === opts.expectedEpoch;
|
|
135
|
+
const sessionMatches = opts.expectedSession === undefined
|
|
136
|
+
|| (opts.expectedSession === '' ? owner === undefined : owner?.sessionId === opts.expectedSession);
|
|
137
|
+
if (!epochMatches || !sessionMatches) {
|
|
138
|
+
return { status: 'busy', epoch: currentEpoch };
|
|
139
|
+
}
|
|
140
|
+
const epoch = currentEpoch + 1;
|
|
141
|
+
let result;
|
|
142
|
+
try {
|
|
143
|
+
result = await lifecycle({ sessionId: identity.sessionId, epoch });
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
// Withdraw: the claim itself persisted nothing. Remove only rows carrying this exact claim
|
|
147
|
+
// token (session + channel + epoch) — whatever the lifecycle itself ensured — then restore
|
|
148
|
+
// the captured standing rows so the old owner survives byte-consistently. Foreign rows and
|
|
149
|
+
// later owners are never touched.
|
|
150
|
+
for (const scope of ['user', 'local']) {
|
|
151
|
+
const tokenRows = await ledger(env, scope).listPresence({
|
|
152
|
+
location,
|
|
153
|
+
participant: name,
|
|
154
|
+
session: identity.sessionId,
|
|
155
|
+
scopes: [scope],
|
|
156
|
+
});
|
|
157
|
+
for (const row of tokenRows) {
|
|
158
|
+
if (row.channel !== identity.channel || presenceEpoch(row) !== epoch)
|
|
159
|
+
continue;
|
|
160
|
+
await ledger(env, scope).removePresence({
|
|
161
|
+
location: row.location,
|
|
162
|
+
participant: row.participant,
|
|
163
|
+
session: row.session,
|
|
164
|
+
channel: row.channel,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
for (const row of standing[scope]) {
|
|
168
|
+
await ledger(env, scope).ensurePresence(row, scope);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
throw error;
|
|
172
|
+
}
|
|
173
|
+
// Finalize: the lifecycle committed. Replace the standing owner with the new owner's row at
|
|
174
|
+
// the claim epoch in every scope where the owner was visible, so exactly one current owner
|
|
175
|
+
// remains — also when the takeover is a self-takeover (same session as the standing owner).
|
|
176
|
+
for (const scope of ['user', 'local']) {
|
|
177
|
+
for (const row of standing[scope]) {
|
|
178
|
+
await ledger(env, scope).removePresence({
|
|
179
|
+
location: row.location,
|
|
180
|
+
participant: row.participant,
|
|
181
|
+
session: row.session,
|
|
182
|
+
channel: row.channel,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
await ledger(env, scope).ensurePresence({
|
|
186
|
+
location,
|
|
187
|
+
participant: name,
|
|
188
|
+
session: identity.sessionId,
|
|
189
|
+
channel: identity.channel,
|
|
190
|
+
updatedAt: Date.now(),
|
|
191
|
+
epoch,
|
|
192
|
+
}, scope);
|
|
193
|
+
}
|
|
194
|
+
return { status: 'acquired', sessionId: identity.sessionId, epoch, result };
|
|
195
|
+
});
|
|
59
196
|
}
|
|
60
197
|
export async function releaseSessionParticipant(squarePath, name, env = process.env) {
|
|
61
198
|
const identity = localSessionIdentities(env)[0];
|
package/dist/routes.d.ts
CHANGED
|
@@ -13,7 +13,6 @@ export interface WakeRouteCapabilities {
|
|
|
13
13
|
readonly canUse: (kind: WakeRouteKind, address: Readonly<Record<string, string>>) => boolean;
|
|
14
14
|
}
|
|
15
15
|
export declare function defaultWakeRouteCapabilities(hostLedger?: import('./host-ledger.js').HostLedgerPort): Promise<WakeRouteCapabilities>;
|
|
16
|
-
/** Pure, ordered route precedence. Capability predicates own adapter and scope checks. */
|
|
17
16
|
export declare function selectPrimaryWakeRoute(input: {
|
|
18
17
|
readonly boundary: WakeBoundary;
|
|
19
18
|
readonly env: NodeJS.ProcessEnv;
|
|
@@ -33,12 +32,25 @@ export declare function upsertWakeRoute(route: Omit<WakeRoute, 'updatedAt'>, opt
|
|
|
33
32
|
at?: number;
|
|
34
33
|
env?: NodeJS.ProcessEnv;
|
|
35
34
|
}): Promise<void>;
|
|
36
|
-
export
|
|
35
|
+
export type RouteEpoch = {
|
|
36
|
+
readonly epoch?: number;
|
|
37
|
+
};
|
|
38
|
+
export declare function applyWakeRouteToState(state: import('./model.js').SquareState, route: Omit<WakeRoute, 'updatedAt'> & RouteEpoch, at?: number): void;
|
|
39
|
+
export declare function dropSessionWakeRoutesFromState(state: import('./model.js').SquareState, location: string, sessionId: string): void;
|
|
40
|
+
export declare function dropParticipantWakeRoutesFromState(state: import('./model.js').SquareState, location: string, participant: string): void;
|
|
41
|
+
export declare function sessionOwnsParticipantRoutes(state: import('./model.js').SquareState, location: string, participant: string, sessionId: string): boolean;
|
|
42
|
+
export declare function sessionCanEndParticipant(state: import('./model.js').SquareState, location: string, participant: string, sessionId: string, currentSessionId?: string): boolean;
|
|
43
|
+
export declare function publishWakeRoute(artifact: import('./ports.js').SquareArtifactPort, route: Omit<WakeRoute, 'updatedAt'> & RouteEpoch, opts?: {
|
|
37
44
|
at?: number;
|
|
45
|
+
requireCurrentSession?: boolean;
|
|
38
46
|
}): Promise<void>;
|
|
39
47
|
export declare function retireWakeRoute(route: WakeRoute, opts?: {
|
|
40
48
|
at?: number;
|
|
41
49
|
env?: NodeJS.ProcessEnv;
|
|
50
|
+
expectedEpoch?: number;
|
|
42
51
|
}): Promise<void>;
|
|
43
|
-
export declare function retireWakeRouteFromArtifact(artifact: import('./ports.js').SquareArtifactPort, route: Pick<WakeRoute, 'location' | 'participant' | 'sessionId'
|
|
52
|
+
export declare function retireWakeRouteFromArtifact(artifact: import('./ports.js').SquareArtifactPort, route: Pick<WakeRoute, 'location' | 'participant' | 'sessionId'>, opts?: {
|
|
53
|
+
readonly expectedEpoch?: number;
|
|
54
|
+
}): Promise<void>;
|
|
55
|
+
export declare function retireWakeRoutesForSessionFromArtifact(artifact: import('./ports.js').SquareArtifactPort, route: Pick<WakeRoute, 'location' | 'sessionId'>): Promise<void>;
|
|
44
56
|
export declare function canonicalRouteLocation(location: string): Promise<string>;
|
package/dist/routes.js
CHANGED
|
@@ -3,6 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import { nameKey } from './model.js';
|
|
4
4
|
import { openSquare } from './square-file-adapter.js';
|
|
5
5
|
import { closeOpenSquare } from './open-square.js';
|
|
6
|
+
import { isCurrentlyJoined } from './runtime.js';
|
|
6
7
|
export { WAKE_ROUTE_KINDS } from './model.js';
|
|
7
8
|
export const ROUTE_FRESH_MS = 24 * 60 * 60 * 1000;
|
|
8
9
|
export async function defaultWakeRouteCapabilities(hostLedger) {
|
|
@@ -39,7 +40,6 @@ function nativeCandidate(boundary) {
|
|
|
39
40
|
return { kind: 'pi-extension', address: { sessionId: boundary.sessionId } };
|
|
40
41
|
return undefined;
|
|
41
42
|
}
|
|
42
|
-
/** Pure, ordered route precedence. Capability predicates own adapter and scope checks. */
|
|
43
43
|
export function selectPrimaryWakeRoute(input) {
|
|
44
44
|
const { boundary, env, capabilities } = input;
|
|
45
45
|
const paseoAgentId = env.PASEO_AGENT_ID?.trim();
|
|
@@ -87,24 +87,84 @@ export async function upsertWakeRoute(route, opts = {}) {
|
|
|
87
87
|
const location = await canonicalRouteLocation(route.location);
|
|
88
88
|
await withArtifact(location, async (square) => publishWakeRoute(square.artifact, { ...route, location }, opts));
|
|
89
89
|
}
|
|
90
|
+
function sameRouteAddress(left, right) {
|
|
91
|
+
const leftKeys = Object.keys(left).sort();
|
|
92
|
+
const rightKeys = Object.keys(right).sort();
|
|
93
|
+
return leftKeys.length === rightKeys.length && leftKeys.every((key, index) => rightKeys[index] === key && left[key] === right[key]);
|
|
94
|
+
}
|
|
95
|
+
function canonicalRouteLocationSync(location) {
|
|
96
|
+
const absolute = path.resolve(location);
|
|
97
|
+
try {
|
|
98
|
+
return fs.realpathSync(absolute);
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return absolute;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
export function applyWakeRouteToState(state, route, at = Date.now()) {
|
|
105
|
+
const location = canonicalRouteLocationSync(route.location);
|
|
106
|
+
state.routes = [...(state.routes ?? []).filter((item) => {
|
|
107
|
+
const itemLocation = canonicalRouteLocationSync(item.location);
|
|
108
|
+
return !(itemLocation === location && routeIdentityKey(item, itemLocation) === routeIdentityKey(route, location));
|
|
109
|
+
}), { ...route, location, updatedAt: at }];
|
|
110
|
+
}
|
|
111
|
+
export function dropSessionWakeRoutesFromState(state, location, sessionId) {
|
|
112
|
+
const canonical = canonicalRouteLocationSync(location);
|
|
113
|
+
state.routes = (state.routes ?? []).filter((item) => !(item.sessionId === sessionId && canonicalRouteLocationSync(item.location) === canonical));
|
|
114
|
+
}
|
|
115
|
+
export function dropParticipantWakeRoutesFromState(state, location, participant) {
|
|
116
|
+
const canonical = canonicalRouteLocationSync(location);
|
|
117
|
+
state.routes = (state.routes ?? []).filter((item) => !(nameKey(item.participant) === nameKey(participant) && canonicalRouteLocationSync(item.location) === canonical));
|
|
118
|
+
}
|
|
119
|
+
export function sessionOwnsParticipantRoutes(state, location, participant, sessionId) {
|
|
120
|
+
const canonical = canonicalRouteLocationSync(location);
|
|
121
|
+
const owned = (state.routes ?? []).filter((item) => nameKey(item.participant) === nameKey(participant) && canonicalRouteLocationSync(item.location) === canonical);
|
|
122
|
+
return owned.length > 0 && owned.every((item) => item.sessionId === sessionId);
|
|
123
|
+
}
|
|
124
|
+
export function sessionCanEndParticipant(state, location, participant, sessionId, currentSessionId) {
|
|
125
|
+
const canonical = canonicalRouteLocationSync(location);
|
|
126
|
+
const owned = (state.routes ?? []).filter((item) => nameKey(item.participant) === nameKey(participant) && canonicalRouteLocationSync(item.location) === canonical);
|
|
127
|
+
if (owned.length === 0)
|
|
128
|
+
return currentSessionId === sessionId;
|
|
129
|
+
return owned.every((item) => item.sessionId === sessionId) && (currentSessionId === undefined || currentSessionId === sessionId);
|
|
130
|
+
}
|
|
90
131
|
export async function publishWakeRoute(artifact, route, opts = {}) {
|
|
91
132
|
const location = await canonicalRouteLocation(route.location);
|
|
92
|
-
|
|
133
|
+
const at = opts.at ?? Date.now();
|
|
134
|
+
await artifact.transact((state) => {
|
|
135
|
+
if (opts.requireCurrentSession) {
|
|
136
|
+
const participantRoutes = (state.routes ?? []).filter((item) => nameKey(item.participant) === nameKey(route.participant) && canonicalRouteLocationSync(item.location) === location);
|
|
137
|
+
if (!isCurrentlyJoined(state.acts, route.participant) || participantRoutes.some((item) => item.sessionId !== route.sessionId))
|
|
138
|
+
return { state, result: undefined };
|
|
139
|
+
}
|
|
140
|
+
const identity = routeIdentityKey({ ...route, location });
|
|
141
|
+
const matching = (state.routes ?? []).filter((item) => routeIdentityKey(item, canonicalRouteLocationSync(item.location)) === identity);
|
|
142
|
+
const existing = matching[0];
|
|
143
|
+
const existingEpoch = existing?.epoch;
|
|
144
|
+
const epochMatches = route.epoch === undefined ? existingEpoch === undefined : existingEpoch === route.epoch;
|
|
145
|
+
if (matching.length === 1 && existing !== undefined && existing.updatedAt > at - ROUTE_FRESH_MS && existing.kind === route.kind && existing.channel === route.channel && sameRouteAddress(existing.address, route.address) && epochMatches)
|
|
146
|
+
return { result: undefined };
|
|
147
|
+
return { state: { ...state, routes: [...(state.routes ?? []).filter((item) => routeIdentityKey(item, canonicalRouteLocationSync(item.location)) !== identity), { ...route, location, updatedAt: at }] }, result: undefined };
|
|
148
|
+
});
|
|
93
149
|
}
|
|
94
150
|
export async function retireWakeRoute(route, opts = {}) {
|
|
95
151
|
const location = await canonicalRouteLocation(route.location);
|
|
96
|
-
await withArtifact(location, async (square) => retireWakeRouteFromArtifact(square.artifact, { ...route, location }));
|
|
152
|
+
await withArtifact(location, async (square) => retireWakeRouteFromArtifact(square.artifact, { ...route, location }, opts));
|
|
97
153
|
}
|
|
98
|
-
export async function retireWakeRouteFromArtifact(artifact, route) {
|
|
154
|
+
export async function retireWakeRouteFromArtifact(artifact, route, opts = {}) {
|
|
99
155
|
const location = await canonicalRouteLocation(route.location);
|
|
100
|
-
|
|
156
|
+
const target = routeIdentityKey({ ...route, location });
|
|
157
|
+
await artifact.transact((state) => ({ state: { ...state, routes: (state.routes ?? []).filter((item) => {
|
|
158
|
+
const itemLocation = canonicalRouteLocationSync(item.location);
|
|
159
|
+
if (routeIdentityKey(item, itemLocation) !== target)
|
|
160
|
+
return true;
|
|
161
|
+
const itemEpoch = item.epoch;
|
|
162
|
+
return opts.expectedEpoch !== undefined && itemEpoch !== opts.expectedEpoch;
|
|
163
|
+
}) }, result: undefined }));
|
|
164
|
+
}
|
|
165
|
+
export async function retireWakeRoutesForSessionFromArtifact(artifact, route) {
|
|
166
|
+
await artifact.transact((state) => { dropSessionWakeRoutesFromState(state, route.location, route.sessionId); return { state, result: undefined }; });
|
|
101
167
|
}
|
|
102
168
|
export async function canonicalRouteLocation(location) {
|
|
103
|
-
|
|
104
|
-
try {
|
|
105
|
-
return await fs.promises.realpath(absolute);
|
|
106
|
-
}
|
|
107
|
-
catch {
|
|
108
|
-
return absolute;
|
|
109
|
-
}
|
|
169
|
+
return canonicalRouteLocationSync(location);
|
|
110
170
|
}
|
package/dist/runtime.d.ts
CHANGED
|
@@ -5,7 +5,6 @@ export declare const SLEEP_MS: number;
|
|
|
5
5
|
export declare const STALE_MS: number;
|
|
6
6
|
export declare const WATCH_STALE_MS: number;
|
|
7
7
|
export declare const LOCK_RETRY_MS: number;
|
|
8
|
-
export declare const LOCK_STALE_MS: number;
|
|
9
8
|
export declare const THROTTLE_WINDOW_MS: number;
|
|
10
9
|
export declare const UNREAD_BLOCK_GRACE_MS: number;
|
|
11
10
|
export declare function nowMs(): number;
|
package/dist/runtime.js
CHANGED
|
@@ -38,7 +38,6 @@ export const SLEEP_MS = parseIntegerEnvAlias('SQUARE_WATCH_POLL_MS', ['SQUARE_SL
|
|
|
38
38
|
export const STALE_MS = parseIntegerEnvAlias('SQUARE_WATCH_QUIET_MS', ['SQUARE_STALE_MS'], scaledMs(WATCH_HEARTBEAT_MS, parseScaleEnv('SQUARE_WATCH_QUIET_SCALE', 5)));
|
|
39
39
|
export const WATCH_STALE_MS = parseIntegerEnv('SQUARE_WATCH_STALE_MS', scaledMs(WATCH_HEARTBEAT_MS, parseScaleEnv('SQUARE_WATCH_STALE_SCALE', 3)));
|
|
40
40
|
export const LOCK_RETRY_MS = parseIntegerEnv('SQUARE_LOCK_RETRY_MS', 25);
|
|
41
|
-
export const LOCK_STALE_MS = parseIntegerEnv('SQUARE_LOCK_STALE_MS', 30000);
|
|
42
41
|
export const THROTTLE_WINDOW_MS = parseIntegerEnv('SQUARE_THROTTLE_WINDOW_MS', 60000);
|
|
43
42
|
export const UNREAD_BLOCK_GRACE_MS = parseIntegerEnv('SQUARE_UNREAD_BLOCK_GRACE_MS', 90000);
|
|
44
43
|
export function nowMs() {
|
package/dist/square-actions.d.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { type SquareState } from './model.js';
|
|
2
2
|
import type { HostLedgerPort, SquareArtifactPort } from './ports.js';
|
|
3
|
-
import type { Activity, ExpressOptions, ExpressResult } from './square-facade.js';
|
|
3
|
+
import type { Activity, CatchOptions, CatchResult, ExpressOptions, ExpressResult, OwnershipFenceOptions } from './square-facade.js';
|
|
4
4
|
import { type CatchProjection } from './catch-decisions.js';
|
|
5
|
-
import type { CatchOptions, CatchResult } from './square-facade.js';
|
|
6
5
|
export interface OperationContext {
|
|
7
6
|
readonly artifact: SquareArtifactPort;
|
|
8
7
|
readonly clock: () => number;
|
|
@@ -11,15 +10,17 @@ export interface OperationContext {
|
|
|
11
10
|
readonly wakeTransport?: import('./ports.js').WakeTransportPort;
|
|
12
11
|
readonly env?: NodeJS.ProcessEnv;
|
|
13
12
|
}
|
|
13
|
+
export type { OwnershipFenceOptions };
|
|
14
14
|
export declare function catchUp(square: OperationContext, name: string, options?: CatchOptions, project?: (state: SquareState) => CatchProjection): Promise<CatchResult>;
|
|
15
15
|
export declare function join(square: OperationContext, name: string): Promise<{
|
|
16
16
|
readonly name: string;
|
|
17
17
|
readonly activity: Activity | null;
|
|
18
18
|
}>;
|
|
19
19
|
/** End the standing participant and immediately let the caller reclaim the name. */
|
|
20
|
-
export declare function takeover(square: OperationContext, name: string): Promise<{
|
|
20
|
+
export declare function takeover(square: OperationContext, name: string, _oldSessionIds?: readonly string[]): Promise<{
|
|
21
21
|
readonly name: string;
|
|
22
22
|
readonly activities: readonly Activity[];
|
|
23
|
+
readonly epoch?: number;
|
|
23
24
|
}>;
|
|
24
25
|
export declare function implicitJoin(square: OperationContext, name: string): Promise<{
|
|
25
26
|
readonly name: string;
|
|
@@ -33,6 +34,8 @@ export interface ListenerChangeResult {
|
|
|
33
34
|
export declare function listen(square: OperationContext, actor: string, target: string): Promise<ListenerChangeResult>;
|
|
34
35
|
export declare function ignore(square: OperationContext, actor: string, target: string): Promise<ListenerChangeResult>;
|
|
35
36
|
export declare function listening(square: OperationContext, actor: string): Promise<readonly string[]>;
|
|
36
|
-
export declare function done(square: OperationContext, name: string, body?: string): Promise<ExpressResult>;
|
|
37
|
+
export declare function done(square: OperationContext, name: string, body?: string, fence?: OwnershipFenceOptions): Promise<ExpressResult>;
|
|
38
|
+
/** SessionEnd retires the ended session's routes even when its presence row is gone. */
|
|
39
|
+
export declare function endOwnedSession(square: OperationContext, name: string, sessionId: string, expectedEpoch?: number): Promise<ExpressResult | null>;
|
|
37
40
|
export declare function hold(square: OperationContext, name: string, reason?: string): Promise<ExpressResult>;
|
|
38
41
|
export declare function resume(square: OperationContext, name: string): Promise<ExpressResult>;
|