@astrosheep/square 0.3.29 → 0.3.31
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/claude-plugin/hooks/hooks.json +0 -3
- package/claude-plugin/skills/square/SKILL.md +8 -7
- package/codex-plugin/.codex-plugin/plugin.json +1 -1
- package/dist/activity.js +7 -3
- package/dist/artifact.js +7 -50
- package/dist/automatic-session.js +31 -14
- package/dist/boundary-presentation.d.ts +1 -1
- package/dist/boundary-presentation.js +58 -14
- package/dist/catch-decisions.d.ts +17 -0
- package/dist/catch-decisions.js +53 -0
- package/dist/claude-hook.d.ts +1 -1
- package/dist/cli/context.js +10 -2
- package/dist/cli/observation-commands.d.ts +5 -1
- package/dist/cli/observation-commands.js +68 -33
- package/dist/cli/square-commands.js +17 -10
- package/dist/codex-hook.d.ts +1 -1
- package/dist/decisions.js +2 -2
- package/dist/delivery-health.d.ts +1 -1
- package/dist/delivery-operations.d.ts +25 -0
- package/dist/delivery-operations.js +124 -0
- package/dist/help.js +1 -1
- package/dist/host-ledger-file-adapter.d.ts +34 -0
- package/dist/host-ledger-file-adapter.js +165 -0
- package/dist/host-ledger.d.ts +160 -0
- package/dist/host-ledger.js +1 -0
- package/dist/inbox.d.ts +2 -2
- package/dist/inbox.js +22 -14
- package/dist/index.d.ts +4 -1
- package/dist/index.js +1 -0
- package/dist/landing.d.ts +18 -14
- package/dist/landing.js +29 -113
- package/dist/model.d.ts +6 -17
- package/dist/notifications.d.ts +6 -10
- package/dist/notifications.js +71 -192
- package/dist/open-square.d.ts +4 -4
- package/dist/open-square.js +1 -1
- package/dist/ports.d.ts +127 -0
- package/dist/ports.js +1 -0
- package/dist/presence.d.ts +1 -2
- package/dist/presence.js +13 -46
- package/dist/presentation-operations.d.ts +3 -0
- package/dist/presentation-operations.js +51 -0
- package/dist/presentation.d.ts +2 -2
- package/dist/presentation.js +11 -7
- package/dist/presented.d.ts +1 -12
- package/dist/presented.js +6 -75
- package/dist/registry.d.ts +10 -10
- package/dist/registry.js +48 -113
- package/dist/routes.d.ts +6 -33
- package/dist/routes.js +6 -170
- package/dist/runtime.d.ts +1 -1
- package/dist/runtime.js +3 -4
- package/dist/square-actions.d.ts +32 -0
- package/dist/square-actions.js +167 -0
- package/dist/square-facade.d.ts +7 -7
- package/dist/square-file-adapter.d.ts +4 -3
- package/dist/square-file-adapter.js +22 -5
- package/dist/square-projections.d.ts +68 -0
- package/dist/square-projections.js +87 -0
- package/dist/square-storage.d.ts +2 -2
- package/dist/square-storage.js +14 -9
- package/dist/square-wiring.d.ts +3 -3
- package/dist/square-wiring.js +44 -29
- package/dist/views.d.ts +8 -2
- package/dist/views.js +28 -24
- package/dist/wake-attempts.d.ts +29 -22
- package/dist/wake-attempts.js +36 -119
- package/dist/wake-evidence.d.ts +6 -18
- package/dist/wake-evidence.js +17 -80
- package/dist/wakes.d.ts +2 -16
- package/dist/wakes.js +7 -27
- package/dist/watch.js +2 -3
- package/extensions/square-pi.js +7 -2
- package/package.json +1 -1
- package/skills/brainstorm/SKILL.md +2 -2
- package/skills/square/SKILL.md +8 -7
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { withFileLock } from './file-lock.js';
|
|
5
|
+
import { nameKey } from './model.js';
|
|
6
|
+
import { isCurrentlyJoined } from './runtime.js';
|
|
7
|
+
const LOCK = { retryMs: 10, staleMs: 300000 };
|
|
8
|
+
const RETENTION = 7 * 86400000;
|
|
9
|
+
async function canon(value) { const absolute = path.resolve(value); try {
|
|
10
|
+
return await fs.realpath(absolute);
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return absolute;
|
|
14
|
+
} }
|
|
15
|
+
async function read(file, now, includeFuture = false) { try {
|
|
16
|
+
return (await fs.readFile(file, 'utf8')).split('\n').flatMap(line => { try {
|
|
17
|
+
const row = JSON.parse(line);
|
|
18
|
+
const at = row.at ?? row.updatedAt;
|
|
19
|
+
return row.v === 1 && typeof at === 'number' && at >= now - RETENTION && (includeFuture || at <= now) ? [row] : [];
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return [];
|
|
23
|
+
} });
|
|
24
|
+
}
|
|
25
|
+
catch (e) {
|
|
26
|
+
if (e.code === 'ENOENT')
|
|
27
|
+
return [];
|
|
28
|
+
throw e;
|
|
29
|
+
} }
|
|
30
|
+
async function write(file, rows) { await fs.mkdir(path.dirname(file), { recursive: true }); const tmp = `${file}.${process.pid}.${Date.now()}.tmp`; await fs.writeFile(tmp, rows.length ? rows.map(row => JSON.stringify(row)).join('\n') + '\n' : '', { mode: 0o600 }); await fs.rename(tmp, file); }
|
|
31
|
+
function k(v) { return JSON.stringify([v.location, nameKey(v.participant), v.session, v.channel, v.activity, v.kind]); }
|
|
32
|
+
function evidenceKey(v) { return JSON.stringify([v.location, nameKey(v.participant), v.session, v.activity, v.kind, v.attemptN ?? null]); }
|
|
33
|
+
async function readClaims(file, now) { try {
|
|
34
|
+
return (await fs.readFile(file, 'utf8')).split('\n').flatMap(line => { try {
|
|
35
|
+
const row = JSON.parse(line);
|
|
36
|
+
return row.v === 1 && typeof row.ts === 'number' && typeof row.attention_key === 'string' && typeof row.leaseId === 'string' && typeof row.expiresAt === 'number' && (row.phase === 'claimed' || row.phase === 'dispatching') && row.ts >= now - RETENTION ? [row] : [];
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return [];
|
|
40
|
+
} });
|
|
41
|
+
}
|
|
42
|
+
catch (e) {
|
|
43
|
+
if (e.code === 'ENOENT')
|
|
44
|
+
return [];
|
|
45
|
+
throw e;
|
|
46
|
+
} }
|
|
47
|
+
function processAlive(pid) { if (pid === undefined)
|
|
48
|
+
return undefined; try {
|
|
49
|
+
process.kill(pid, 0);
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
return error.code === 'ESRCH' ? false : true;
|
|
54
|
+
} }
|
|
55
|
+
async function writeClaims(file, rows) { await fs.mkdir(path.dirname(file), { recursive: true }); const tmp = `${file}.${process.pid}.${Date.now()}.tmp`; await fs.writeFile(tmp, rows.length ? rows.map(row => JSON.stringify(row)).join('\n') + '\n' : '', { mode: 0o600 }); await fs.rename(tmp, file); }
|
|
56
|
+
export class FileHostLedgerPort {
|
|
57
|
+
user;
|
|
58
|
+
local;
|
|
59
|
+
writable;
|
|
60
|
+
readable;
|
|
61
|
+
claims;
|
|
62
|
+
clock;
|
|
63
|
+
constructor(o = {}) { const testRoot = process.env.SQUARE_REGISTRY ? path.dirname(process.env.SQUARE_REGISTRY) : undefined; this.user = o.userPath ?? (testRoot ?? path.join(os.homedir(), '.square', 'host-ledger')); this.local = o.localPath ?? (testRoot ?? path.join(process.cwd(), '.square', 'host-ledger')); this.writable = o.writableScope ?? 'local'; this.readable = o.readableScopes ?? ['user', 'local']; this.claims = o.claimsPath ?? path.join(this.user, 'wake-claims.ndjsonl'); this.clock = o.now ?? Date.now; }
|
|
64
|
+
file(s, f) { return path.join(s === 'user' ? this.user : this.local, `${f}.ndjsonl`); }
|
|
65
|
+
async ensurePresence(i, scope = this.writable) { const r = { ...i, location: await canon(i.location), ...(scope === 'local' ? { route: undefined } : {}), updatedAt: i.updatedAt ?? this.clock(), v: 1 }; const f = this.file(scope, 'presence'); try {
|
|
66
|
+
await withFileLock(f + '.lock', LOCK, async () => write(f, [...(await read(f, this.clock(), true)).filter(x => k(x) !== k(r)), r]));
|
|
67
|
+
return { status: 'ensured', record: r };
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
return { status: 'degraded', record: r, error };
|
|
71
|
+
} }
|
|
72
|
+
async removePresence(i) { const r = { ...i, location: await canon(i.location) }, f = this.file(this.writable, 'presence'); await withFileLock(f + '.lock', LOCK, async () => write(f, (await read(f, this.clock(), true)).filter(x => k(x) !== k(r)))); }
|
|
73
|
+
async listPresence(i = {}) { const loc = i.location === undefined ? undefined : await canon(i.location), m = new Map(); for (const s of i.scopes ?? this.readable)
|
|
74
|
+
for (const r of await read(this.file(s, 'presence'), i.now ?? this.clock())) {
|
|
75
|
+
if (loc && r.location !== loc || i.participant && nameKey(r.participant) !== nameKey(i.participant) || i.session && r.session !== i.session)
|
|
76
|
+
continue;
|
|
77
|
+
const x = s === 'user' ? r : { ...r, route: undefined }, key = k(x), old = m.get(key);
|
|
78
|
+
if (old?.scope === 'user' && s === 'local')
|
|
79
|
+
continue;
|
|
80
|
+
if (!old || s === 'user' || x.updatedAt >= old.record.updatedAt)
|
|
81
|
+
m.set(key, { record: x, scope: s });
|
|
82
|
+
} return [...m.values()].map(({ record }) => record); }
|
|
83
|
+
async claimWakeDispatch(i) { const location = await canon(i.attention.squarePath), key = JSON.stringify([location, nameKey(i.attention.recipient), i.attention.actIndex, i.session ?? null, 'wake']), at = i.at ?? this.clock(); return withFileLock(`${this.claims}.lock`, LOCK, async () => { const rows = await readClaims(this.claims, at), existing = rows.find((row) => row.attention_key === key); if (existing?.phase === 'dispatching') {
|
|
84
|
+
const alive = processAlive(existing.ownerPid);
|
|
85
|
+
if (existing.expiresAt <= at || alive === false)
|
|
86
|
+
return { type: 'ambiguous', lease: existing };
|
|
87
|
+
return { type: 'busy' };
|
|
88
|
+
} if (existing !== undefined && existing.expiresAt > at)
|
|
89
|
+
return { type: 'busy' }; const next = { v: 1, ts: at, attention_key: key, leaseId: i.leaseId, expiresAt: at + i.leaseMs, phase: 'claimed', ownerPid: process.pid, ...(i.session === undefined ? {} : { session: i.session }) }; await writeClaims(this.claims, [...rows.filter((row) => row.attention_key !== key), next]); return { type: 'acquired', leaseId: i.leaseId }; }); }
|
|
90
|
+
async transitionWakeDispatch(i) { const location = await canon(i.attention.squarePath), key = JSON.stringify([location, nameKey(i.attention.recipient), i.attention.actIndex, i.session ?? null, 'wake']), at = i.at ?? this.clock(); return withFileLock(`${this.claims}.lock`, LOCK, async () => { const rows = await readClaims(this.claims, at), current = rows.find((row) => { if (row.leaseId !== i.leaseId)
|
|
91
|
+
return false; try {
|
|
92
|
+
const parts = JSON.parse(row.attention_key);
|
|
93
|
+
return parts[0] === location && parts[1] === nameKey(i.attention.recipient) && parts[2] === i.attention.actIndex;
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return false;
|
|
97
|
+
} }); if (current === undefined)
|
|
98
|
+
return false; const next = { v: 1, ts: at, attention_key: key, leaseId: i.leaseId, expiresAt: at + i.leaseMs, phase: i.phase, ownerPid: current.ownerPid, ...(i.routeKind === undefined ? {} : { routeKind: i.routeKind }), ...(i.attemptN === undefined ? {} : { attemptN: i.attemptN }), ...(i.session === undefined ? {} : { session: i.session }) }; await writeClaims(this.claims, [...rows.filter((row) => row !== current), next]); return true; }); }
|
|
99
|
+
async releaseWakeDispatch(i) { const location = await canon(i.attention.squarePath), key = JSON.stringify([location, nameKey(i.attention.recipient), i.attention.actIndex, i.session ?? null, 'wake']), at = i.at ?? this.clock(); await withFileLock(`${this.claims}.lock`, LOCK, async () => { const rows = await readClaims(this.claims, at); await writeClaims(this.claims, rows.filter((row) => { if (row.attention_key === key)
|
|
100
|
+
return false; if (row.leaseId !== i.leaseId)
|
|
101
|
+
return true; try {
|
|
102
|
+
const parts = JSON.parse(row.attention_key);
|
|
103
|
+
return parts[0] !== location || parts[1] !== nameKey(i.attention.recipient) || parts[2] !== i.attention.actIndex || (i.session !== undefined && parts[3] !== i.session);
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return true;
|
|
107
|
+
} })); }); }
|
|
108
|
+
async listWakeAttempts(i = {}) { const attention = i.attention; return this.listEvidence({ kind: 'wake', ...(attention === undefined ? {} : { location: await canon(attention.squarePath), participant: attention.recipient, activity: `act/${attention.actIndex}` }), ...(i.session === undefined ? {} : { session: i.session }), now: i.now }); }
|
|
109
|
+
async appendWakeAttempt(i) { if (i.kind !== 'wake')
|
|
110
|
+
throw new Error('Wake attempt rows require wake evidence kind'); return this.appendEvidence(i); }
|
|
111
|
+
async claimEvidence(i) { const f = this.file('user', 'evidence'), at = i.now ?? this.clock(), { leaseMs, ...claim } = i, r = { ...claim, location: await canon(i.location), outcome: 'dispatching', at, expiresAt: at + leaseMs, v: 1 }; try {
|
|
112
|
+
return await withFileLock(f + '.lock', LOCK, async () => { const all = await read(f, at, true), matching = all.filter(x => x.kind === r.kind && x.location === r.location && nameKey(x.participant) === nameKey(r.participant) && x.activity === r.activity && x.session === r.session), old = matching.findLast(x => x.outcome === 'dispatching'); if (old !== undefined && typeof old.expiresAt === 'number' && old.expiresAt > at)
|
|
113
|
+
return { status: 'busy', record: old }; const delivered = (r.kind === 'wake' && matching.some(x => x.outcome === 'accepted')) || (r.kind === 'presentation' && matching.some(x => x.outcome === 'presented')); if (delivered)
|
|
114
|
+
return { status: 'delivered', record: matching.findLast(x => x.outcome === 'accepted' || x.outcome === 'presented') }; await write(f, [...all.filter(x => !(x.kind === r.kind && x.location === r.location && nameKey(x.participant) === nameKey(r.participant) && x.activity === r.activity && x.session === r.session && x.outcome === 'dispatching')), r]); return { status: 'acquired' }; });
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
return { status: 'degraded', error };
|
|
118
|
+
} }
|
|
119
|
+
async releaseEvidence(i) { const f = this.file('user', 'evidence'), location = await canon(i.location), at = i.now ?? this.clock(); await withFileLock(f + '.lock', LOCK, async () => write(f, (await read(f, at)).filter(x => !(x.kind === i.kind && x.location === location && nameKey(x.participant) === nameKey(i.participant) && x.activity === i.activity && x.session === i.session && x.outcome === 'dispatching')))); }
|
|
120
|
+
async appendEvidence(i) { const f = this.file('user', 'evidence'), r = { ...i, location: await canon(i.location), at: i.at ?? this.clock(), v: 1 }; await withFileLock(f + '.lock', LOCK, async () => { const all = await read(f, r.at, true); const base = all.filter(x => k(x) === k(r)); const replaceable = new Set(base.filter(x => x.outcome === 'dispatching' || (r.attemptN !== undefined && x.attemptN === r.attemptN)).map(x => evidenceKey(x))); await write(f, [...all.filter(x => !(x.outcome === 'dispatching' && k(x) === k(r)) && !replaceable.has(evidenceKey(x))), r]); }); }
|
|
121
|
+
async listEvidence(i = {}) { const loc = i.location === undefined ? undefined : await canon(i.location); return (await read(this.file('user', 'evidence'), i.now ?? this.clock())).filter(r => (!loc || r.location === loc) && (!i.participant || nameKey(r.participant) === nameKey(i.participant)) && (!i.session || r.session === i.session) && (!i.activity || r.activity === i.activity) && (!i.kind || r.kind === i.kind)); }
|
|
122
|
+
async gcEvidence(i) { const f = this.file('user', 'evidence'); await withFileLock(f + '.lock', LOCK, async () => write(f, (await read(f, this.clock())).filter(r => r.at >= i.before))); }
|
|
123
|
+
async reconcileBinding(i = {}) { let b = []; try {
|
|
124
|
+
b = [...await this.listPresence({ scopes: i.scopes, now: i.now })];
|
|
125
|
+
if (i.artifact) {
|
|
126
|
+
const { state } = await i.artifact.read();
|
|
127
|
+
const stale = b.filter(x => !isCurrentlyJoined(state.acts, x.participant));
|
|
128
|
+
for (const x of stale) {
|
|
129
|
+
for (const scope of i.scopes ?? this.readable) {
|
|
130
|
+
const remover = new FileHostLedgerPort({ userPath: this.user, localPath: this.local, writableScope: scope, readableScopes: [scope], now: this.clock });
|
|
131
|
+
await remover.removePresence(x);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
b = b.filter(x => !stale.includes(x));
|
|
135
|
+
}
|
|
136
|
+
const latest = new Map();
|
|
137
|
+
for (const x of b) {
|
|
138
|
+
const key = JSON.stringify([x.location, nameKey(x.participant)]);
|
|
139
|
+
const current = latest.get(key);
|
|
140
|
+
if (current === undefined || ((x.updatedAt ?? 0) > (current.updatedAt ?? 0)))
|
|
141
|
+
latest.set(key, x);
|
|
142
|
+
}
|
|
143
|
+
const winners = [...latest.values()];
|
|
144
|
+
for (const x of b) {
|
|
145
|
+
const winner = latest.get(JSON.stringify([x.location, nameKey(x.participant)]));
|
|
146
|
+
if (winner === undefined || winner.session === x.session)
|
|
147
|
+
continue;
|
|
148
|
+
for (const scope of i.scopes ?? this.readable) {
|
|
149
|
+
const remover = new FileHostLedgerPort({ userPath: this.user, localPath: this.local, writableScope: scope, readableScopes: [scope], now: this.clock });
|
|
150
|
+
await remover.removePresence(x);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
const u = new FileHostLedgerPort({ userPath: this.user, localPath: this.local, writableScope: 'user', readableScopes: ['user'], now: this.clock });
|
|
154
|
+
for (const x of winners) {
|
|
155
|
+
const result = await u.ensurePresence(x);
|
|
156
|
+
if (result.status === 'degraded')
|
|
157
|
+
return { status: 'degraded', bindings: winners, error: result.error };
|
|
158
|
+
}
|
|
159
|
+
return { status: 'reconciled', bindings: winners };
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
return { status: 'degraded', bindings: b, error };
|
|
163
|
+
} }
|
|
164
|
+
}
|
|
165
|
+
export function createHostLedgerPort(o = {}) { return new FileHostLedgerPort(o); }
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import type { WakeRouteKind } from './model.js';
|
|
2
|
+
export type HostLedgerScope = 'user' | 'local';
|
|
3
|
+
export type PresenceChannel = 'claude-code' | 'codex' | 'opencode' | 'pi' | 'paseo' | 'unknown';
|
|
4
|
+
export interface PresenceRecord {
|
|
5
|
+
readonly location: string;
|
|
6
|
+
readonly participant: string;
|
|
7
|
+
readonly session: string;
|
|
8
|
+
readonly channel: PresenceChannel;
|
|
9
|
+
readonly route?: {
|
|
10
|
+
readonly kind: WakeRouteKind;
|
|
11
|
+
readonly address: Readonly<Record<string, string>>;
|
|
12
|
+
};
|
|
13
|
+
readonly updatedAt?: number;
|
|
14
|
+
}
|
|
15
|
+
export type PresenceKey = Pick<PresenceRecord, 'location' | 'participant' | 'session' | 'channel'>;
|
|
16
|
+
export interface PresenceLookup {
|
|
17
|
+
readonly location?: string;
|
|
18
|
+
readonly participant?: string;
|
|
19
|
+
readonly session?: string;
|
|
20
|
+
readonly scopes?: readonly HostLedgerScope[];
|
|
21
|
+
readonly now?: number;
|
|
22
|
+
}
|
|
23
|
+
export type PresenceResult = {
|
|
24
|
+
readonly status: 'ensured';
|
|
25
|
+
readonly record: PresenceRecord;
|
|
26
|
+
} | {
|
|
27
|
+
readonly status: 'degraded';
|
|
28
|
+
readonly record: PresenceRecord;
|
|
29
|
+
readonly error: unknown;
|
|
30
|
+
};
|
|
31
|
+
export interface EvidenceRecord {
|
|
32
|
+
readonly location: string;
|
|
33
|
+
readonly participant: string;
|
|
34
|
+
readonly session: string;
|
|
35
|
+
readonly activity: string;
|
|
36
|
+
readonly kind: 'wake' | 'presentation';
|
|
37
|
+
readonly outcome: string;
|
|
38
|
+
readonly at?: number;
|
|
39
|
+
readonly expiresAt?: number;
|
|
40
|
+
readonly routeKind?: WakeRouteKind;
|
|
41
|
+
readonly signature?: string;
|
|
42
|
+
readonly attemptN?: number;
|
|
43
|
+
readonly message?: string;
|
|
44
|
+
readonly diagnostic?: unknown;
|
|
45
|
+
}
|
|
46
|
+
export interface EvidenceClaim {
|
|
47
|
+
readonly location: string;
|
|
48
|
+
readonly participant: string;
|
|
49
|
+
readonly session: string;
|
|
50
|
+
readonly activity: string;
|
|
51
|
+
readonly kind: EvidenceRecord['kind'];
|
|
52
|
+
readonly leaseMs: number;
|
|
53
|
+
readonly now?: number;
|
|
54
|
+
}
|
|
55
|
+
export type EvidenceRelease = Omit<EvidenceClaim, 'leaseMs'>;
|
|
56
|
+
export interface EvidenceLookup {
|
|
57
|
+
readonly location?: string;
|
|
58
|
+
readonly participant?: string;
|
|
59
|
+
readonly session?: string;
|
|
60
|
+
readonly activity?: string;
|
|
61
|
+
readonly kind?: EvidenceRecord['kind'];
|
|
62
|
+
readonly now?: number;
|
|
63
|
+
}
|
|
64
|
+
export interface EvidenceGc {
|
|
65
|
+
readonly before: number;
|
|
66
|
+
}
|
|
67
|
+
export interface WakeAttention {
|
|
68
|
+
readonly squarePath: string;
|
|
69
|
+
readonly actIndex: number;
|
|
70
|
+
readonly recipient: string;
|
|
71
|
+
}
|
|
72
|
+
export interface WakeDispatchLease {
|
|
73
|
+
readonly leaseId: string;
|
|
74
|
+
readonly expiresAt: number;
|
|
75
|
+
readonly phase: 'claimed' | 'dispatching';
|
|
76
|
+
readonly routeKind?: WakeRouteKind;
|
|
77
|
+
readonly attemptN?: number;
|
|
78
|
+
readonly session?: string;
|
|
79
|
+
}
|
|
80
|
+
export type WakeDispatchClaim = {
|
|
81
|
+
readonly type: 'acquired';
|
|
82
|
+
readonly leaseId: string;
|
|
83
|
+
} | {
|
|
84
|
+
readonly type: 'busy';
|
|
85
|
+
} | {
|
|
86
|
+
readonly type: 'ambiguous';
|
|
87
|
+
readonly lease: WakeDispatchLease;
|
|
88
|
+
};
|
|
89
|
+
export interface WakeDispatchClaimInput {
|
|
90
|
+
readonly attention: WakeAttention;
|
|
91
|
+
readonly leaseId: string;
|
|
92
|
+
readonly leaseMs: number;
|
|
93
|
+
readonly session?: string;
|
|
94
|
+
readonly at?: number;
|
|
95
|
+
}
|
|
96
|
+
export interface WakeDispatchTransitionInput {
|
|
97
|
+
readonly attention: WakeAttention;
|
|
98
|
+
readonly leaseId: string;
|
|
99
|
+
readonly phase: WakeDispatchLease['phase'];
|
|
100
|
+
readonly leaseMs: number;
|
|
101
|
+
readonly routeKind?: WakeRouteKind;
|
|
102
|
+
readonly attemptN?: number;
|
|
103
|
+
readonly session?: string;
|
|
104
|
+
readonly at?: number;
|
|
105
|
+
}
|
|
106
|
+
export interface WakeDispatchReleaseInput {
|
|
107
|
+
readonly attention: WakeAttention;
|
|
108
|
+
readonly leaseId: string;
|
|
109
|
+
readonly session?: string;
|
|
110
|
+
readonly at?: number;
|
|
111
|
+
}
|
|
112
|
+
export interface WakeAttemptLookup {
|
|
113
|
+
readonly attention?: WakeAttention;
|
|
114
|
+
readonly session?: string;
|
|
115
|
+
readonly now?: number;
|
|
116
|
+
}
|
|
117
|
+
export type ClaimResult = {
|
|
118
|
+
readonly status: 'acquired';
|
|
119
|
+
} | {
|
|
120
|
+
readonly status: 'busy' | 'delivered';
|
|
121
|
+
readonly record: EvidenceRecord;
|
|
122
|
+
} | {
|
|
123
|
+
readonly status: 'degraded';
|
|
124
|
+
readonly error: unknown;
|
|
125
|
+
};
|
|
126
|
+
export interface ReconcileBindingInput {
|
|
127
|
+
readonly scopes?: readonly HostLedgerScope[];
|
|
128
|
+
readonly now?: number;
|
|
129
|
+
readonly artifact?: {
|
|
130
|
+
read(): Promise<{
|
|
131
|
+
state: {
|
|
132
|
+
acts: readonly {
|
|
133
|
+
kind: string;
|
|
134
|
+
actor?: string;
|
|
135
|
+
}[];
|
|
136
|
+
};
|
|
137
|
+
}>;
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
export type ReconcileBindingResult = {
|
|
141
|
+
readonly status: 'reconciled' | 'degraded';
|
|
142
|
+
readonly bindings: readonly PresenceRecord[];
|
|
143
|
+
readonly error?: unknown;
|
|
144
|
+
};
|
|
145
|
+
export interface HostLedgerPort {
|
|
146
|
+
ensurePresence(input: PresenceRecord, scope?: HostLedgerScope): Promise<PresenceResult>;
|
|
147
|
+
removePresence(input: PresenceKey): Promise<void>;
|
|
148
|
+
listPresence(input: PresenceLookup): Promise<readonly PresenceRecord[]>;
|
|
149
|
+
claimEvidence(input: EvidenceClaim): Promise<ClaimResult>;
|
|
150
|
+
releaseEvidence(input: EvidenceRelease): Promise<void>;
|
|
151
|
+
appendEvidence(input: EvidenceRecord): Promise<void>;
|
|
152
|
+
listEvidence(input: EvidenceLookup): Promise<readonly EvidenceRecord[]>;
|
|
153
|
+
listWakeAttempts(input?: WakeAttemptLookup): Promise<readonly EvidenceRecord[]>;
|
|
154
|
+
appendWakeAttempt(input: EvidenceRecord): Promise<void>;
|
|
155
|
+
claimWakeDispatch(input: WakeDispatchClaimInput): Promise<WakeDispatchClaim>;
|
|
156
|
+
transitionWakeDispatch(input: WakeDispatchTransitionInput): Promise<boolean>;
|
|
157
|
+
releaseWakeDispatch(input: WakeDispatchReleaseInput): Promise<void>;
|
|
158
|
+
gcEvidence(input: EvidenceGc): Promise<void>;
|
|
159
|
+
reconcileBinding(input: ReconcileBindingInput): Promise<ReconcileBindingResult>;
|
|
160
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/inbox.d.ts
CHANGED
|
@@ -6,6 +6,6 @@ export interface PendingWaitOptions {
|
|
|
6
6
|
/** After a delivery failure, wait for a new state edge before retrying the same pending work. */
|
|
7
7
|
skipImmediate?: boolean;
|
|
8
8
|
}
|
|
9
|
-
export declare function sessionInbox(sessionId: string): Promise<InboxMembership[]>;
|
|
9
|
+
export declare function sessionInbox(sessionId: string, env?: NodeJS.ProcessEnv): Promise<InboxMembership[]>;
|
|
10
10
|
/** Wait for a bound square to produce a new pending notification without consuming it. */
|
|
11
|
-
export declare function waitForSessionPending(sessionId: string, timeoutMs: number, options?: PendingWaitOptions): Promise<InboxMembership[]>;
|
|
11
|
+
export declare function waitForSessionPending(sessionId: string, timeoutMs: number, options?: PendingWaitOptions, env?: NodeJS.ProcessEnv): Promise<InboxMembership[]>;
|
package/dist/inbox.js
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
|
-
import { lookupSessionBindings } from './registry.js';
|
|
2
1
|
import { openSquare } from './square-file-adapter.js';
|
|
3
2
|
import { closeOpenSquare } from './open-square.js';
|
|
4
|
-
import { inboxProjection } from './views.js';
|
|
5
3
|
import { waitForSquareChanges } from './square-file-adapter.js';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { createHostLedgerPort } from './host-ledger-file-adapter.js';
|
|
6
|
+
import { projectPresentation, projectSessionBindings } from './square-projections.js';
|
|
7
|
+
function hostLedgerForEnv(env) {
|
|
8
|
+
const root = env.SQUARE_REGISTRY === undefined ? undefined : path.dirname(env.SQUARE_REGISTRY);
|
|
9
|
+
return createHostLedgerPort({
|
|
10
|
+
userPath: env.SQUARE_HOST_LEDGER_USER ?? root,
|
|
11
|
+
localPath: env.SQUARE_HOST_LEDGER_LOCAL ?? root,
|
|
12
|
+
});
|
|
13
|
+
}
|
|
6
14
|
function notificationKey(membership, actIndex) {
|
|
7
15
|
return `${membership.squarePath}\u0000${membership.name.toLocaleLowerCase()}\u0000${actIndex}`;
|
|
8
16
|
}
|
|
@@ -16,19 +24,19 @@ function withoutExcluded(inbox, excludeKeys) {
|
|
|
16
24
|
}))
|
|
17
25
|
.filter((membership) => membership.notifications.length > 0);
|
|
18
26
|
}
|
|
19
|
-
export async function sessionInbox(sessionId) {
|
|
27
|
+
export async function sessionInbox(sessionId, env = process.env) {
|
|
20
28
|
const inbox = [];
|
|
21
|
-
|
|
29
|
+
const hostLedger = hostLedgerForEnv(env);
|
|
30
|
+
for (const binding of await projectSessionBindings({ hostLedger, sessionId })) {
|
|
22
31
|
let square;
|
|
23
32
|
try {
|
|
24
|
-
square = await openSquare(binding.
|
|
25
|
-
const projection = await
|
|
33
|
+
square = await openSquare(binding.location, { env, hostLedger });
|
|
34
|
+
const projection = await projectPresentation({ artifact: square.artifact, binding, now: square.clock() });
|
|
26
35
|
if (!projection.joined)
|
|
27
36
|
continue;
|
|
28
37
|
inbox.push({
|
|
29
|
-
name: projection.
|
|
30
|
-
squarePath: binding.
|
|
31
|
-
ownerId: binding.ownerId,
|
|
38
|
+
name: projection.binding.participant,
|
|
39
|
+
squarePath: projection.binding.location,
|
|
32
40
|
notifications: [...projection.notifications],
|
|
33
41
|
...(projection.catchLease !== undefined ? { catchLease: projection.catchLease } : {}),
|
|
34
42
|
});
|
|
@@ -44,17 +52,17 @@ export async function sessionInbox(sessionId) {
|
|
|
44
52
|
return inbox;
|
|
45
53
|
}
|
|
46
54
|
/** Wait for a bound square to produce a new pending notification without consuming it. */
|
|
47
|
-
export async function waitForSessionPending(sessionId, timeoutMs, options = {}) {
|
|
55
|
+
export async function waitForSessionPending(sessionId, timeoutMs, options = {}, env = process.env) {
|
|
48
56
|
const deadline = Date.now() + Math.max(0, timeoutMs);
|
|
49
57
|
if (!options.skipImmediate) {
|
|
50
|
-
const immediate = withoutExcluded(await sessionInbox(sessionId), options.excludeKeys);
|
|
58
|
+
const immediate = withoutExcluded(await sessionInbox(sessionId, env), options.excludeKeys);
|
|
51
59
|
if (immediate.some((membership) => membership.notifications.length > 0))
|
|
52
60
|
return immediate;
|
|
53
61
|
}
|
|
54
62
|
if (timeoutMs <= 0 || options.signal?.aborted)
|
|
55
63
|
return [];
|
|
56
|
-
const bindings = await
|
|
57
|
-
const paths = [...new Set(bindings.map((binding) => binding.
|
|
64
|
+
const bindings = await projectSessionBindings({ hostLedger: hostLedgerForEnv(env), sessionId });
|
|
65
|
+
const paths = [...new Set(bindings.map((binding) => binding.location))];
|
|
58
66
|
let aborted = false;
|
|
59
67
|
let projectAfterReady = !options.skipImmediate;
|
|
60
68
|
const onAbort = () => { aborted = true; };
|
|
@@ -67,7 +75,7 @@ export async function waitForSessionPending(sessionId, timeoutMs, options = {})
|
|
|
67
75
|
const change = await waitForSquareChanges(paths, remaining, options.signal, async () => {
|
|
68
76
|
if (!projectAfterReady)
|
|
69
77
|
return undefined;
|
|
70
|
-
const current = withoutExcluded(await sessionInbox(sessionId), options.excludeKeys);
|
|
78
|
+
const current = withoutExcluded(await sessionInbox(sessionId, env), options.excludeKeys);
|
|
71
79
|
return current.some((membership) => membership.notifications.length > 0) ? current : undefined;
|
|
72
80
|
});
|
|
73
81
|
if (aborted || change.status === 'expired')
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
export { Square } from './square-wiring.js';
|
|
2
2
|
export { SquareError } from './model.js';
|
|
3
3
|
export { bindCurrentParticipant, squareAssignedParticipantName, unbindCurrentParticipant } from './registry.js';
|
|
4
|
+
export { createHostLedgerPort, FileHostLedgerPort } from './host-ledger-file-adapter.js';
|
|
5
|
+
export type { HostLedgerPort, PresenceRecord, EvidenceRecord } from './host-ledger.js';
|
|
6
|
+
export type { PresentationSinkPort } from './ports.js';
|
|
4
7
|
export type { ActivityId } from './square-core.js';
|
|
5
|
-
export type { Activity, CatchOptions, CatchResult, ExpressOptions, ExpressResult, HistoryQuery, ListenerChangeResult, OpenOptions, Participant, ParticipantStatus, PerceivedActivity, SquareAtInput, SquareBuildInput, SquareSnapshot, SquareSource,
|
|
8
|
+
export type { Activity, CatchOptions, CatchResult, ExpressOptions, ExpressResult, HistoryQuery, ListenerChangeResult, OpenOptions, Participant, ParticipantStatus, PerceivedActivity, SquareAtInput, SquareBuildInput, SquareSnapshot, SquareSource, } from './square-facade.js';
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export { Square } from './square-wiring.js';
|
|
2
2
|
export { SquareError } from './model.js';
|
|
3
3
|
export { bindCurrentParticipant, squareAssignedParticipantName, unbindCurrentParticipant } from './registry.js';
|
|
4
|
+
export { createHostLedgerPort, FileHostLedgerPort } from './host-ledger-file-adapter.js';
|
package/dist/landing.d.ts
CHANGED
|
@@ -1,22 +1,26 @@
|
|
|
1
|
+
import type { SquareArtifactPort } from './ports.js';
|
|
1
2
|
import type { OpenSquare } from './open-square.js';
|
|
3
|
+
import { type ListenerChangeResult } from './square-actions.js';
|
|
2
4
|
import type { Activity, ExpressOptions, ExpressResult } from './square-facade.js';
|
|
3
|
-
|
|
5
|
+
/** Transitional adapter for existing internal callers until they migrate to SquareArtifactPort. */
|
|
6
|
+
type LegacySquare = {
|
|
7
|
+
readonly cell: SquareArtifactPort;
|
|
8
|
+
readonly clock: () => number;
|
|
9
|
+
};
|
|
10
|
+
export declare function join(square: OpenSquare | LegacySquare, name: string): Promise<{
|
|
4
11
|
readonly name: string;
|
|
5
12
|
readonly activity: Activity | null;
|
|
6
13
|
}>;
|
|
7
|
-
|
|
8
|
-
export declare function implicitJoin(square: OpenSquare, name: string): Promise<{
|
|
14
|
+
export declare function implicitJoin(square: OpenSquare | LegacySquare, name: string): Promise<{
|
|
9
15
|
readonly name: string;
|
|
10
|
-
readonly state:
|
|
16
|
+
readonly state: "joined" | "active" | "done";
|
|
11
17
|
readonly activity: Activity | null;
|
|
12
18
|
}>;
|
|
13
|
-
export declare function express(square: OpenSquare, name: string, body: string, options?: ExpressOptions): Promise<ExpressResult>;
|
|
14
|
-
export
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
export declare function
|
|
18
|
-
export declare function
|
|
19
|
-
export declare function
|
|
20
|
-
export
|
|
21
|
-
export declare function hold(square: OpenSquare, name: string, reason?: string): Promise<ExpressResult>;
|
|
22
|
-
export declare function resume(square: OpenSquare, name: string): Promise<ExpressResult>;
|
|
19
|
+
export declare function express(square: OpenSquare | LegacySquare, name: string, body: string, options?: ExpressOptions): Promise<ExpressResult>;
|
|
20
|
+
export declare function listen(square: OpenSquare | LegacySquare, actor: string, target: string): Promise<ListenerChangeResult>;
|
|
21
|
+
export declare function ignore(square: OpenSquare | LegacySquare, actor: string, target: string): Promise<ListenerChangeResult>;
|
|
22
|
+
export declare function listening(square: OpenSquare | LegacySquare, actor: string): Promise<readonly string[]>;
|
|
23
|
+
export declare function done(square: OpenSquare | LegacySquare, name: string, body?: string): Promise<ExpressResult>;
|
|
24
|
+
export declare function hold(square: OpenSquare | LegacySquare, name: string, reason?: string): Promise<ExpressResult>;
|
|
25
|
+
export declare function resume(square: OpenSquare | LegacySquare, name: string): Promise<ExpressResult>;
|
|
26
|
+
export {};
|
package/dist/landing.js
CHANGED
|
@@ -1,126 +1,42 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
function storeActs(state, acts) {
|
|
7
|
-
const stored = [];
|
|
8
|
-
for (const act of acts) {
|
|
9
|
-
const item = { ...act, index: state.runtime.nextActIndex };
|
|
10
|
-
state.runtime.nextActIndex += 1;
|
|
11
|
-
state.acts.push(item);
|
|
12
|
-
stored.push(item);
|
|
13
|
-
}
|
|
14
|
-
return stored;
|
|
15
|
-
}
|
|
16
|
-
function committedActivity(stored, verb) {
|
|
17
|
-
const activity = stored[0];
|
|
18
|
-
if (activity === undefined)
|
|
19
|
-
throw new Error(`${verb} activity did not commit`);
|
|
20
|
-
return activity;
|
|
21
|
-
}
|
|
22
|
-
function exposeActivity(stored) {
|
|
23
|
-
if (stored.kind === 'read' || stored.actor === undefined)
|
|
24
|
-
throw new Error(`Cannot expose stored activity ${formatActivityId(stored.index)}`);
|
|
1
|
+
import { done as applyDone, express as applyExpress, hold as applyHold, ignore as applyIgnore, implicitJoin as applyImplicitJoin, join as applyJoin, listen as applyListen, listening as applyListening, resume as applyResume, } from './square-actions.js';
|
|
2
|
+
function artifactOf(square) {
|
|
3
|
+
if ('artifact' in square)
|
|
4
|
+
return square.artifact;
|
|
5
|
+
const { cell } = square;
|
|
25
6
|
return {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
...(stored.kind === 'say' && stored.reply !== undefined ? { reply: formatActivityId(stored.reply) } : {}),
|
|
7
|
+
read: () => cell.read(),
|
|
8
|
+
transact: (fn) => cell.transact(fn),
|
|
9
|
+
changed: (sinceVersion, timeoutMs) => cell.changed(sinceVersion, timeoutMs),
|
|
10
|
+
close: () => cell.close(),
|
|
31
11
|
};
|
|
32
12
|
}
|
|
33
|
-
function
|
|
34
|
-
|
|
35
|
-
if (index === undefined)
|
|
36
|
-
throw new SquareError('invalid_args', `Invalid activity id: ${id}`);
|
|
37
|
-
return index;
|
|
38
|
-
}
|
|
39
|
-
async function wakeAfterCommit(square, recipients, activity) {
|
|
40
|
-
if (recipients.length === 0)
|
|
41
|
-
return;
|
|
42
|
-
try {
|
|
43
|
-
square.notifier?.wake(recipients, activity);
|
|
44
|
-
}
|
|
45
|
-
catch { /* post-commit effects cannot undo activities */ }
|
|
13
|
+
function contextOf(square) {
|
|
14
|
+
return { artifact: artifactOf(square), clock: square.clock, ...('location' in square ? { location: square.location, hostLedger: square.hostLedger, env: square.env } : {}) };
|
|
46
15
|
}
|
|
47
|
-
export
|
|
48
|
-
|
|
49
|
-
const committed = await square.cell.transact((state) => {
|
|
50
|
-
const decision = decideJoin(state, name, now);
|
|
51
|
-
if (decision.joinAct === undefined)
|
|
52
|
-
return { result: { name: decision.joinedName, stored: null } };
|
|
53
|
-
return { state, result: { name: decision.joinedName, stored: committedActivity(storeActs(state, [decision.joinAct]), 'join') } };
|
|
54
|
-
});
|
|
55
|
-
return { name: committed.name, activity: committed.stored === null ? null : exposeActivity(committed.stored) };
|
|
16
|
+
export function join(square, name) {
|
|
17
|
+
return applyJoin(contextOf(square), name);
|
|
56
18
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const now = square.clock();
|
|
60
|
-
const committed = await square.cell.transact((state) => {
|
|
61
|
-
const decision = decideImplicitJoin(state, name, now);
|
|
62
|
-
if (decision.joinAct === undefined)
|
|
63
|
-
return { result: { name: decision.joinedName, state: decision.state, stored: null } };
|
|
64
|
-
return { state, result: { name: decision.joinedName, state: decision.state, stored: committedActivity(storeActs(state, [decision.joinAct]), 'join') } };
|
|
65
|
-
});
|
|
66
|
-
return { name: committed.name, state: committed.state, activity: committed.stored === null ? null : exposeActivity(committed.stored) };
|
|
19
|
+
export function implicitJoin(square, name) {
|
|
20
|
+
return applyImplicitJoin(contextOf(square), name);
|
|
67
21
|
}
|
|
68
|
-
export
|
|
69
|
-
|
|
70
|
-
const reply = options.reply === undefined ? undefined : parseRequiredActivityId(options.reply);
|
|
71
|
-
const committed = await square.cell.transact((state) => {
|
|
72
|
-
const decision = decideAct(state, { name, body, force: options.force ?? false, now, ...(options.reach === undefined ? {} : { reach: options.reach }), ...(reply === undefined ? {} : { reply }) });
|
|
73
|
-
if (decision.type === 'blocked') {
|
|
74
|
-
const pending = decision.activitySummaries.reduce((count, summary) => count + summary.count, 0) + decision.unreadRoomChanges.length;
|
|
75
|
-
throw new SquareError('behind', `${participantIdentity(name)} has pending activity`, { pending });
|
|
76
|
-
}
|
|
77
|
-
if (decision.type === 'held') {
|
|
78
|
-
const holder = state.acts.filter((activity) => activity.kind === 'hold').at(-1)?.actor;
|
|
79
|
-
throw new SquareError('held', 'The square is held', holder === undefined ? undefined : { holder });
|
|
80
|
-
}
|
|
81
|
-
if (decision.type === 'capped')
|
|
82
|
-
throw new SquareError('capped', `${participantIdentity(name)} reached the activity cap`);
|
|
83
|
-
if (decision.type === 'throttled')
|
|
84
|
-
throw new SquareError('throttled', `${name} is throttled`, { retryAfterMs: decision.delayMs });
|
|
85
|
-
if (decision.type === 'bell_quota')
|
|
86
|
-
throw new SquareError('bell_quota', `${participantIdentity(name)} cannot ring the bell yet`, { retryAfterMs: Math.max(1, decision.nextAt - now) });
|
|
87
|
-
const stored = committedActivity(storeActs(state, [decision.act]), 'express');
|
|
88
|
-
return { state, result: { stored, recipients: deriveDeliveryModel(state).plan(stored).map((notification) => notification.recipient) } };
|
|
89
|
-
});
|
|
90
|
-
const activity = exposeActivity(committed.stored);
|
|
91
|
-
await wakeAfterCommit(square, committed.recipients, activity);
|
|
92
|
-
return { activity };
|
|
93
|
-
}
|
|
94
|
-
async function landListenerChange(square, verb, actor, target) {
|
|
95
|
-
const now = square.clock();
|
|
96
|
-
const stored = await square.cell.transact((state) => {
|
|
97
|
-
const act = verb === 'listen'
|
|
98
|
-
? coreListen(state, actor, target, now)
|
|
99
|
-
: coreIgnore(state, actor, target, now);
|
|
100
|
-
if (act === undefined)
|
|
101
|
-
return { result: null };
|
|
102
|
-
return { state, result: committedActivity(storeActs(state, [act]), verb) };
|
|
103
|
-
});
|
|
104
|
-
return { activity: stored === null ? null : exposeActivity(stored) };
|
|
22
|
+
export function express(square, name, body, options) {
|
|
23
|
+
return applyExpress(contextOf(square), name, body, options);
|
|
105
24
|
}
|
|
106
25
|
export function listen(square, actor, target) {
|
|
107
|
-
return
|
|
26
|
+
return applyListen(contextOf(square), actor, target);
|
|
108
27
|
}
|
|
109
28
|
export function ignore(square, actor, target) {
|
|
110
|
-
return
|
|
29
|
+
return applyIgnore(contextOf(square), actor, target);
|
|
30
|
+
}
|
|
31
|
+
export function listening(square, actor) {
|
|
32
|
+
return applyListening(contextOf(square), actor);
|
|
33
|
+
}
|
|
34
|
+
export function done(square, name, body = '') {
|
|
35
|
+
return applyDone(contextOf(square), name, body);
|
|
111
36
|
}
|
|
112
|
-
export
|
|
113
|
-
|
|
114
|
-
return coreListening(state, actor);
|
|
37
|
+
export function hold(square, name, reason = '') {
|
|
38
|
+
return applyHold(contextOf(square), name, reason);
|
|
115
39
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
const stored = await square.cell.transact((state) => {
|
|
119
|
-
const act = verb === 'done' ? coreDone(state, actor, body, now) : verb === 'hold' ? coreHold(state, actor, body, now) : coreResume(state, actor, now);
|
|
120
|
-
return { state, result: committedActivity(storeActs(state, [act]), verb) };
|
|
121
|
-
});
|
|
122
|
-
return { activity: exposeActivity(stored) };
|
|
40
|
+
export function resume(square, name) {
|
|
41
|
+
return applyResume(contextOf(square), name);
|
|
123
42
|
}
|
|
124
|
-
export function done(square, name, body = '') { return landCore(square, 'done', name, body); }
|
|
125
|
-
export function hold(square, name, reason = '') { return landCore(square, 'hold', name, reason); }
|
|
126
|
-
export function resume(square, name) { return landCore(square, 'resume', name); }
|