@astrosheep/square 0.3.94 → 0.3.96
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/artifact.js +1 -1
- package/dist/attention-presentation.d.ts +5 -4
- package/dist/attention-presentation.js +18 -8
- package/dist/attention.d.ts +19 -0
- package/dist/attention.js +30 -0
- package/dist/boundary-presentation.js +50 -48
- package/dist/catch-decisions.d.ts +1 -2
- package/dist/catch-decisions.js +41 -21
- package/dist/channel.d.ts +72 -0
- package/dist/channel.js +86 -0
- package/dist/claude-delivery.js +2 -1
- package/dist/cli/context.js +0 -2
- package/dist/cli/express-presentation.js +3 -3
- package/dist/cli/json.js +1 -1
- package/dist/cli/program.js +1 -1
- package/dist/cli/registry.js +1 -3
- package/dist/cli/square-commands.d.ts +0 -2
- package/dist/cli/square-commands.js +21 -27
- package/dist/decisions.d.ts +0 -5
- package/dist/decisions.js +0 -13
- package/dist/delivery-attempts.d.ts +52 -0
- package/dist/delivery-attempts.js +73 -0
- package/dist/delivery-effects.d.ts +139 -0
- package/dist/delivery-effects.js +163 -0
- package/dist/delivery-operations.js +43 -68
- package/dist/delivery-primitives.d.ts +5 -0
- package/dist/delivery-primitives.js +3 -0
- package/dist/delivery.d.ts +1 -1
- package/dist/delivery.js +8 -7
- package/dist/help.js +4 -6
- package/dist/http-delivery.d.ts +16 -0
- package/dist/http-delivery.js +101 -0
- package/dist/inbox.js +3 -1
- package/dist/mcp.js +65 -63
- package/dist/model.d.ts +10 -6
- package/dist/model.js +20 -1
- package/dist/notifications.js +2 -0
- package/dist/operation-presentation.d.ts +1 -2
- package/dist/operation-presentation.js +12 -20
- package/dist/ports.d.ts +8 -14
- package/dist/presentation-operations.d.ts +15 -19
- package/dist/presentation-operations.js +47 -57
- package/dist/presentation.d.ts +2 -9
- package/dist/presentation.js +13 -48
- package/dist/receive.d.ts +66 -0
- package/dist/receive.js +54 -0
- package/dist/runtime.d.ts +0 -1
- package/dist/runtime.js +2 -3
- package/dist/sqlite-square.d.ts +3 -2
- package/dist/sqlite-square.js +83 -57
- package/dist/square-api.d.ts +1 -1
- package/dist/square-api.js +1 -1
- package/dist/square-core.d.ts +1 -9
- package/dist/square-domain.js +29 -36
- package/dist/square-facade.d.ts +25 -23
- package/dist/square-legacy-framing.d.ts +2 -0
- package/dist/square-legacy-framing.js +8 -0
- package/dist/square-migration.d.ts +3 -1
- package/dist/square-migration.js +46 -13
- package/dist/square-receive.d.ts +6 -0
- package/dist/square-receive.js +18 -0
- package/dist/square-sqlite.d.ts +5 -3
- package/dist/square-sqlite.js +100 -92
- package/dist/square-tools.d.ts +1 -0
- package/dist/square-tools.js +12 -12
- package/dist/views.d.ts +2 -3
- package/dist/views.js +14 -19
- package/dist/wakes.d.ts +0 -3
- package/dist/wakes.js +3 -5
- package/examples/symmetric-domains.mjs +250 -0
- package/guides/application-facade-delivery-evidence.md +5 -5
- package/guides/attention-primitive.md +32 -0
- package/guides/change-waits.md +2 -2
- package/guides/codex-delivery.md +1 -1
- package/guides/delivery-primitives.md +180 -0
- package/guides/mcp.md +1 -1
- package/guides/migration.md +12 -6
- package/guides/participant-ownership.md +1 -1
- package/guides/pi-tools.md +2 -2
- package/guides/primitive-catch-consolidation.md +5 -5
- package/guides/primitive-history-consolidation.md +2 -2
- package/guides/primitive-status-consolidation.md +3 -3
- package/guides/primitive-storage-consolidation.md +1 -1
- package/guides/receive-primitive.md +90 -0
- package/guides/row-runtime-delivery-evidence.md +1 -1
- package/guides/row-storage-design.md +17 -23
- package/guides/square-api.md +77 -10
- package/guides/symmetric-domains.md +92 -0
- package/package.json +18 -1
- package/plugins/claude/.claude-plugin/plugin.json +1 -1
- package/plugins/claude/skills/square/SKILL.md +2 -6
- package/plugins/codex/.codex-plugin/plugin.json +1 -1
- package/plugins/paseo/client/marble.ts +7 -0
- package/plugins/paseo/client/markdown.tsx +65 -9
- package/plugins/paseo/client/palette.ts +43 -20
- package/plugins/paseo/client/square-message.tsx +57 -48
- package/plugins/paseo/shared/message.ts +1 -2
- package/plugins/pi/square-pi.js +6 -4
- package/skills/square/SKILL.md +2 -6
package/dist/cli/registry.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { buildCommand, doneCommand, expressCommand,
|
|
1
|
+
import { buildCommand, doneCommand, expressCommand, ignoreCommand, joinCommand, listenCommand, listeningCommand, unlistenCommand } from './square-commands.js';
|
|
2
2
|
import { doctorCommand, migrateCommand } from './maintenance-commands.js';
|
|
3
3
|
import { harnessCommand, installCommand, uninstallCommand } from './harness-command.js';
|
|
4
4
|
import { helpCommand, versionCommand } from './meta-commands.js';
|
|
@@ -21,8 +21,6 @@ export const commandRegistry = [
|
|
|
21
21
|
{ names: ['unlisten'], spec: unlistenCommand },
|
|
22
22
|
{ names: ['listening'], spec: listeningCommand },
|
|
23
23
|
{ names: ['done'], spec: doneCommand },
|
|
24
|
-
{ names: ['hold'], spec: holdCommand },
|
|
25
|
-
{ names: ['resume'], spec: resumeCommand },
|
|
26
24
|
{ names: ['install'], spec: installCommand },
|
|
27
25
|
{ names: ['uninstall'], spec: uninstallCommand },
|
|
28
26
|
{ names: ['harness'], spec: harnessCommand },
|
|
@@ -35,6 +35,4 @@ export declare const ignoreCommand: CommandSpec<ListenerIntent, string>;
|
|
|
35
35
|
export declare const unlistenCommand: CommandSpec<ListenerIntent, string>;
|
|
36
36
|
export declare const listeningCommand: CommandSpec<ListenerIntent, string>;
|
|
37
37
|
export declare const doneCommand: CommandSpec<BodyIntent, string>;
|
|
38
|
-
export declare const holdCommand: CommandSpec<BodyIntent, string>;
|
|
39
|
-
export declare const resumeCommand: CommandSpec<BodyIntent, string>;
|
|
40
38
|
export {};
|
|
@@ -54,7 +54,7 @@ export const buildCommand = {
|
|
|
54
54
|
const cap = data.hardCap === null ? 'unlimited' : formatHardCap(data.hardCap);
|
|
55
55
|
const throttle = data.throttlePerMinute === undefined ? [] : [` · throttle ${data.throttlePerMinute}/min`];
|
|
56
56
|
return withPathOutput(square.location, ['· the square stands ready', ` · cap ${cap}`, ...throttle].join('\n'), {
|
|
57
|
-
participantCount: 0,
|
|
57
|
+
participantCount: 0,
|
|
58
58
|
});
|
|
59
59
|
},
|
|
60
60
|
present: (result) => process.stdout.write(result),
|
|
@@ -182,29 +182,23 @@ export const listeningCommand = {
|
|
|
182
182
|
},
|
|
183
183
|
present: (result) => process.stdout.write(result),
|
|
184
184
|
};
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
const
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
present: (result) => process.stdout.write(result),
|
|
206
|
-
};
|
|
207
|
-
}
|
|
208
|
-
export const doneCommand = lifecycleCommand('done');
|
|
209
|
-
export const holdCommand = lifecycleCommand('hold');
|
|
210
|
-
export const resumeCommand = lifecycleCommand('resume');
|
|
185
|
+
export const doneCommand = {
|
|
186
|
+
parse(argv, context) {
|
|
187
|
+
const args = argv.filter((arg) => arg !== '--json');
|
|
188
|
+
if (args.length > 1)
|
|
189
|
+
usage(context.command);
|
|
190
|
+
return { body: args[0] };
|
|
191
|
+
},
|
|
192
|
+
async execute(intent, context) {
|
|
193
|
+
const body = intent.body === '-' || ((intent.body === undefined || intent.body === '') && !process.stdin.isTTY)
|
|
194
|
+
? await readStdin() : intent.body;
|
|
195
|
+
const squarePath = requireSquarePath(context);
|
|
196
|
+
const result = await withSquare(context, async (square) => {
|
|
197
|
+
const participant = await selectOwnSeat(square, commandIdentity(context.env, context.owner).owner, context.name);
|
|
198
|
+
const options = context.json ? {} : { observe: true };
|
|
199
|
+
return participant.done(body, options);
|
|
200
|
+
});
|
|
201
|
+
return context.json ? jsonResult(result) : renderLifecycleResult(squarePath, result);
|
|
202
|
+
},
|
|
203
|
+
present: (result) => process.stdout.write(result),
|
|
204
|
+
};
|
package/dist/decisions.d.ts
CHANGED
|
@@ -26,9 +26,6 @@ export type ActDecision = ({
|
|
|
26
26
|
} | {
|
|
27
27
|
type: 'throttled';
|
|
28
28
|
delayMs: number;
|
|
29
|
-
} | {
|
|
30
|
-
type: 'held';
|
|
31
|
-
reason: string | undefined;
|
|
32
29
|
} | {
|
|
33
30
|
type: 'bell_quota';
|
|
34
31
|
nextAt: number;
|
|
@@ -52,8 +49,6 @@ export declare function coreListen(facts: SquareRead, actor: string, target: str
|
|
|
52
49
|
export declare function coreIgnore(facts: SquareRead, actor: string, target: string, now: number): TimedAct<"listen" | "ignore" | "unlisten"> | undefined;
|
|
53
50
|
export declare function coreUnlisten(facts: SquareRead, actor: string, target: string, now: number): TimedAct<"listen" | "ignore" | "unlisten"> | undefined;
|
|
54
51
|
export declare function coreListening(facts: SquareRead, actor: string): readonly string[];
|
|
55
|
-
export declare function coreHold(facts: SquareRead, actor: string, body: string, now: number): TimedAct<'hold'>;
|
|
56
|
-
export declare function coreResume(facts: SquareRead, actor: string, now: number): TimedAct<'resume'>;
|
|
57
52
|
export declare function compareParticipantActivity(a: Pick<ParticipantSummary, 'name' | 'lastActiveAt'>, b: Pick<ParticipantSummary, 'name' | 'lastActiveAt'>): number;
|
|
58
53
|
export declare function coreParticipants(facts: SquareRead, now: number): ParticipantSummary[];
|
|
59
54
|
export {};
|
package/dist/decisions.js
CHANGED
|
@@ -130,9 +130,6 @@ export function decideAct(facts, input) {
|
|
|
130
130
|
throw activityRefusal('already_done', `${name} is already done`);
|
|
131
131
|
if (!current.joined)
|
|
132
132
|
throw activityRefusal('not_joined', `${name} has not joined this square`);
|
|
133
|
-
const hold = facts.hold();
|
|
134
|
-
if (hold.active)
|
|
135
|
-
return { type: 'held', reason: hold.reason };
|
|
136
133
|
if (header.hardCap !== null && current.sayCount >= header.hardCap)
|
|
137
134
|
return { type: 'capped', count: current.sayCount, hardCap: header.hardCap };
|
|
138
135
|
if (reach === 'bell') {
|
|
@@ -185,16 +182,6 @@ export function coreListening(facts, actor) {
|
|
|
185
182
|
const current = resolveStanding(facts, actor);
|
|
186
183
|
return facts.attention(current.name).filter((item) => item.kind === 'listen').map((item) => item.target);
|
|
187
184
|
}
|
|
188
|
-
export function coreHold(facts, actor, body, now) {
|
|
189
|
-
const current = resolveStanding(facts, actor);
|
|
190
|
-
requireStanding(current);
|
|
191
|
-
return { kind: 'hold', actor: current.name, at: now, body: body.replace(/\r\n/g, '\n').trim() };
|
|
192
|
-
}
|
|
193
|
-
export function coreResume(facts, actor, now) {
|
|
194
|
-
const current = resolveStanding(facts, actor);
|
|
195
|
-
requireStanding(current);
|
|
196
|
-
return { kind: 'resume', actor: current.name, at: now };
|
|
197
|
-
}
|
|
198
185
|
export function compareParticipantActivity(a, b) {
|
|
199
186
|
return (b.lastActiveAt ?? -Infinity) - (a.lastActiveAt ?? -Infinity) || a.name.localeCompare(b.name);
|
|
200
187
|
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export type DeliveryPurpose = 'dispatch' | 'presentation';
|
|
2
|
+
export type AttemptOutcome = 'dispatching' | 'accepted' | 'failed' | 'unknown' | 'released' | 'presented' | 'clipped';
|
|
3
|
+
export interface Attempt<D> {
|
|
4
|
+
readonly purpose: DeliveryPurpose;
|
|
5
|
+
readonly token: string;
|
|
6
|
+
readonly ordinal: number;
|
|
7
|
+
readonly outcome: AttemptOutcome;
|
|
8
|
+
readonly at: number;
|
|
9
|
+
readonly expiresAt?: number;
|
|
10
|
+
readonly data: D;
|
|
11
|
+
}
|
|
12
|
+
/** One item/recipient/authority/purpose series, bound inside the caller's transaction.
|
|
13
|
+
* The caller owns token uniqueness, isolation and rollback; replacements retain coordinates.
|
|
14
|
+
*/
|
|
15
|
+
export interface AttemptRows<D> {
|
|
16
|
+
list(): readonly Attempt<D>[];
|
|
17
|
+
byToken(token: string): Attempt<D> | undefined;
|
|
18
|
+
insert(attempt: Attempt<D>): void;
|
|
19
|
+
replace(attempt: Attempt<D>): void;
|
|
20
|
+
}
|
|
21
|
+
export type AttemptClaim<D> = {
|
|
22
|
+
readonly status: 'acquired';
|
|
23
|
+
readonly token: string;
|
|
24
|
+
readonly ordinal: number;
|
|
25
|
+
} | {
|
|
26
|
+
readonly status: 'busy' | 'terminal';
|
|
27
|
+
readonly evidence: Attempt<D>;
|
|
28
|
+
readonly interrupted?: true;
|
|
29
|
+
};
|
|
30
|
+
export declare function claimAttempt<D>(rows: AttemptRows<D>, input: {
|
|
31
|
+
purpose: DeliveryPurpose;
|
|
32
|
+
token: string;
|
|
33
|
+
at: number;
|
|
34
|
+
leaseMs: number;
|
|
35
|
+
data: D;
|
|
36
|
+
}): AttemptClaim<D>;
|
|
37
|
+
export declare function prepareAttempt<D>(rows: AttemptRows<D>, input: {
|
|
38
|
+
token: string;
|
|
39
|
+
data: D;
|
|
40
|
+
}): boolean;
|
|
41
|
+
export declare function settleAttempt<D>(rows: AttemptRows<D>, input: {
|
|
42
|
+
token: string;
|
|
43
|
+
outcome: Exclude<AttemptOutcome, 'dispatching'>;
|
|
44
|
+
at: number;
|
|
45
|
+
data: D;
|
|
46
|
+
}): boolean;
|
|
47
|
+
type Custody = Pick<Attempt<unknown>, 'purpose' | 'outcome' | 'expiresAt'>;
|
|
48
|
+
/** Presentation custody suppresses dispatch independently of dispatch acceptance. */
|
|
49
|
+
export declare function presentationBlocksDispatch(attempts: readonly Custody[], now: number): boolean;
|
|
50
|
+
/** An expired dispatch is uncertain, not permission for automatic replay. */
|
|
51
|
+
export declare function automaticDispatchEligible(attempts: readonly Custody[], now: number): boolean;
|
|
52
|
+
export {};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
function finite(value) {
|
|
2
|
+
if (typeof value !== 'number' || !Number.isFinite(value))
|
|
3
|
+
throw new RangeError('time must be finite');
|
|
4
|
+
}
|
|
5
|
+
function token(value) {
|
|
6
|
+
if (typeof value !== 'string' || value.length === 0)
|
|
7
|
+
throw new TypeError('token must be nonempty');
|
|
8
|
+
}
|
|
9
|
+
export function claimAttempt(rows, input) {
|
|
10
|
+
if (input.purpose !== 'dispatch' && input.purpose !== 'presentation')
|
|
11
|
+
throw new TypeError('unsupported purpose');
|
|
12
|
+
token(input.token);
|
|
13
|
+
finite(input.at);
|
|
14
|
+
if (!Number.isSafeInteger(input.leaseMs) || input.leaseMs <= 0)
|
|
15
|
+
throw new RangeError('leaseMs must be a positive safe integer');
|
|
16
|
+
const expiresAt = input.at + input.leaseMs;
|
|
17
|
+
finite(expiresAt);
|
|
18
|
+
const attempts = rows.list();
|
|
19
|
+
const terminal = attempts.find(row => row.outcome === 'unknown'
|
|
20
|
+
|| row.outcome === (input.purpose === 'dispatch' ? 'accepted' : 'presented'));
|
|
21
|
+
if (terminal !== undefined)
|
|
22
|
+
return { status: 'terminal', evidence: terminal };
|
|
23
|
+
const dispatching = attempts.find(row => row.outcome === 'dispatching');
|
|
24
|
+
if (dispatching !== undefined) {
|
|
25
|
+
if (dispatching.expiresAt === undefined || dispatching.expiresAt > input.at)
|
|
26
|
+
return { status: 'busy', evidence: dispatching };
|
|
27
|
+
const { expiresAt: _expiry, ...previous } = dispatching;
|
|
28
|
+
const evidence = { ...previous, outcome: 'unknown', at: input.at };
|
|
29
|
+
rows.replace(evidence);
|
|
30
|
+
return { status: 'terminal', evidence, interrupted: true };
|
|
31
|
+
}
|
|
32
|
+
const ordinal = (attempts[0]?.ordinal ?? -1) + 1;
|
|
33
|
+
if (!Number.isSafeInteger(ordinal) || ordinal < 0)
|
|
34
|
+
throw new RangeError('attempt ordinal exhausted');
|
|
35
|
+
rows.insert({ purpose: input.purpose, token: input.token, ordinal, outcome: 'dispatching', at: input.at, expiresAt, data: input.data });
|
|
36
|
+
return { status: 'acquired', token: input.token, ordinal };
|
|
37
|
+
}
|
|
38
|
+
export function prepareAttempt(rows, input) {
|
|
39
|
+
token(input.token);
|
|
40
|
+
const previous = rows.byToken(input.token);
|
|
41
|
+
if (previous?.outcome !== 'dispatching')
|
|
42
|
+
return false;
|
|
43
|
+
rows.replace({ ...previous, data: input.data });
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
export function settleAttempt(rows, input) {
|
|
47
|
+
token(input.token);
|
|
48
|
+
finite(input.at);
|
|
49
|
+
if (!['accepted', 'failed', 'unknown', 'released', 'presented', 'clipped'].includes(input.outcome))
|
|
50
|
+
throw new TypeError('unsupported completion');
|
|
51
|
+
const previous = rows.byToken(input.token);
|
|
52
|
+
if (previous === undefined)
|
|
53
|
+
return false;
|
|
54
|
+
if (previous.outcome === 'accepted' && input.outcome === 'accepted')
|
|
55
|
+
return true;
|
|
56
|
+
if (previous.outcome !== 'dispatching' && !(previous.outcome === 'unknown' && input.outcome === 'accepted'))
|
|
57
|
+
return false;
|
|
58
|
+
const { expiresAt: _expiry, ...retained } = previous;
|
|
59
|
+
rows.replace({ ...retained, outcome: input.outcome, at: input.at, data: input.data });
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
/** Presentation custody suppresses dispatch independently of dispatch acceptance. */
|
|
63
|
+
export function presentationBlocksDispatch(attempts, now) {
|
|
64
|
+
finite(now);
|
|
65
|
+
return attempts.some(row => row.purpose === 'presentation'
|
|
66
|
+
&& (row.outcome === 'presented' || row.outcome === 'clipped' || row.outcome === 'unknown'
|
|
67
|
+
|| row.outcome === 'dispatching' && (row.expiresAt === undefined || row.expiresAt > now)));
|
|
68
|
+
}
|
|
69
|
+
/** An expired dispatch is uncertain, not permission for automatic replay. */
|
|
70
|
+
export function automaticDispatchEligible(attempts, now) {
|
|
71
|
+
return !presentationBlocksDispatch(attempts, now) && !attempts.some(row => row.purpose === 'dispatch'
|
|
72
|
+
&& (row.outcome === 'accepted' || row.outcome === 'unknown' || row.outcome === 'dispatching'));
|
|
73
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
export interface EffectClaim {
|
|
2
|
+
readonly token: string;
|
|
3
|
+
readonly ordinal: number;
|
|
4
|
+
}
|
|
5
|
+
/** Bound to one captured item/recipient/authority/purpose, not an encoded series key. */
|
|
6
|
+
export interface EffectPort<C> {
|
|
7
|
+
/** The caller's existing atomic claim operation. Undefined means no custody acquired. */
|
|
8
|
+
claim(signal?: AbortSignal): Promise<EffectClaim | undefined>;
|
|
9
|
+
/** Pre-effect policy only. Rejection means unverifiable/not current. */
|
|
10
|
+
current(signal?: AbortSignal): Promise<boolean>;
|
|
11
|
+
/**
|
|
12
|
+
* No request signal: persistence has its own bounded lifetime. For presentation,
|
|
13
|
+
* `presented` MUST complete the attempt and record ALL captured full-consumption
|
|
14
|
+
* receipts in ONE atomic transaction (rollback both on error). `clipped`, `failed`,
|
|
15
|
+
* `unknown` and `released` complete the attempt without full-consumption receipts.
|
|
16
|
+
* No post-effect currentness/deferral/cancellation veto; captured authority still fences.
|
|
17
|
+
*/
|
|
18
|
+
settle(claim: EffectClaim, completion: C): Promise<boolean>;
|
|
19
|
+
}
|
|
20
|
+
export type Persistence = {
|
|
21
|
+
readonly status: 'recorded';
|
|
22
|
+
} | {
|
|
23
|
+
readonly status: 'not-recorded';
|
|
24
|
+
} | {
|
|
25
|
+
readonly status: 'error';
|
|
26
|
+
readonly error: unknown;
|
|
27
|
+
};
|
|
28
|
+
export interface Settlement<C> {
|
|
29
|
+
readonly completion: C;
|
|
30
|
+
readonly persistence: Persistence;
|
|
31
|
+
}
|
|
32
|
+
export interface DispatchOutcome {
|
|
33
|
+
readonly outcome: 'accepted' | 'failed' | 'unknown' | 'not-capable';
|
|
34
|
+
readonly data?: unknown;
|
|
35
|
+
}
|
|
36
|
+
export type DispatchCompletion = {
|
|
37
|
+
readonly outcome: 'accepted' | 'failed' | 'unknown';
|
|
38
|
+
readonly reason: 'returned';
|
|
39
|
+
readonly data?: unknown;
|
|
40
|
+
} | {
|
|
41
|
+
readonly outcome: 'unknown';
|
|
42
|
+
readonly reason: 'threw';
|
|
43
|
+
readonly error: unknown;
|
|
44
|
+
} | {
|
|
45
|
+
readonly outcome: 'unknown';
|
|
46
|
+
readonly reason: 'timeout';
|
|
47
|
+
} | {
|
|
48
|
+
readonly outcome: 'released';
|
|
49
|
+
readonly reason: 'not-current';
|
|
50
|
+
readonly phase: 'initial' | 'final';
|
|
51
|
+
} | {
|
|
52
|
+
readonly outcome: 'released';
|
|
53
|
+
readonly reason: 'not-capable';
|
|
54
|
+
readonly data?: unknown;
|
|
55
|
+
} | {
|
|
56
|
+
readonly outcome: 'released';
|
|
57
|
+
readonly reason: 'cancelled';
|
|
58
|
+
};
|
|
59
|
+
export interface DispatchContext {
|
|
60
|
+
readonly claim: EffectClaim;
|
|
61
|
+
readonly timeoutMs: number;
|
|
62
|
+
readonly deadline: number;
|
|
63
|
+
readonly signal?: AbortSignal;
|
|
64
|
+
/** Policy only. Sender MUST recheck deadline/cancellation after this await before I/O. */
|
|
65
|
+
beforeSend(): Promise<boolean>;
|
|
66
|
+
}
|
|
67
|
+
export interface DispatchReport {
|
|
68
|
+
readonly observation: {
|
|
69
|
+
readonly kind: 'not-invoked';
|
|
70
|
+
} | {
|
|
71
|
+
readonly kind: 'returned';
|
|
72
|
+
readonly result: DispatchOutcome;
|
|
73
|
+
} | {
|
|
74
|
+
readonly kind: 'threw';
|
|
75
|
+
readonly error: unknown;
|
|
76
|
+
} | {
|
|
77
|
+
readonly kind: 'timeout';
|
|
78
|
+
};
|
|
79
|
+
readonly settlement?: Settlement<DispatchCompletion>;
|
|
80
|
+
}
|
|
81
|
+
/** Timer bounds the external attempt only, not claim/currentness/persistence. No automatic retry. */
|
|
82
|
+
export declare function dispatchEffect(port: EffectPort<DispatchCompletion>, send: (context: DispatchContext) => Promise<DispatchOutcome>, options: {
|
|
83
|
+
readonly timeoutMs: number;
|
|
84
|
+
readonly signal?: AbortSignal;
|
|
85
|
+
}): Promise<DispatchReport>;
|
|
86
|
+
export type PresentationCompletion = {
|
|
87
|
+
readonly outcome: 'presented' | 'clipped';
|
|
88
|
+
readonly reason: 'returned';
|
|
89
|
+
} | {
|
|
90
|
+
readonly outcome: 'failed' | 'unknown';
|
|
91
|
+
readonly reason: 'threw';
|
|
92
|
+
readonly error: unknown;
|
|
93
|
+
} | {
|
|
94
|
+
readonly outcome: 'released';
|
|
95
|
+
readonly reason: 'not-current' | 'cancelled' | 'unused';
|
|
96
|
+
};
|
|
97
|
+
export interface PresentationWork<P> {
|
|
98
|
+
readonly payload: P;
|
|
99
|
+
readonly coverage: 'full' | 'clipped';
|
|
100
|
+
readonly port: EffectPort<PresentationCompletion>;
|
|
101
|
+
}
|
|
102
|
+
declare const preparedBrand: unique symbol;
|
|
103
|
+
export interface PreparedPresentation<P> {
|
|
104
|
+
readonly [preparedBrand]: true;
|
|
105
|
+
readonly payload: P;
|
|
106
|
+
eligible(signal?: AbortSignal): Promise<boolean>;
|
|
107
|
+
release(): Promise<Settlement<PresentationCompletion>>;
|
|
108
|
+
}
|
|
109
|
+
export interface PresentationControl {
|
|
110
|
+
readonly signal?: AbortSignal;
|
|
111
|
+
/** Default unknown; throwing or invalid classifiers also stay unknown. */
|
|
112
|
+
readonly classifyError?: (error: unknown) => 'failed' | 'unknown';
|
|
113
|
+
}
|
|
114
|
+
export interface PresentationReport<P, R> {
|
|
115
|
+
readonly observation: {
|
|
116
|
+
readonly kind: 'not-invoked';
|
|
117
|
+
} | {
|
|
118
|
+
readonly kind: 'returned';
|
|
119
|
+
readonly value: R;
|
|
120
|
+
} | {
|
|
121
|
+
readonly kind: 'threw';
|
|
122
|
+
readonly error: unknown;
|
|
123
|
+
readonly outcome: 'failed' | 'unknown';
|
|
124
|
+
};
|
|
125
|
+
readonly settlements: readonly {
|
|
126
|
+
readonly prepared: PreparedPresentation<P>;
|
|
127
|
+
readonly result: Settlement<PresentationCompletion>;
|
|
128
|
+
}[];
|
|
129
|
+
}
|
|
130
|
+
export declare function preparePresentation<P>(work: PresentationWork<P>, control?: {
|
|
131
|
+
readonly signal?: AbortSignal;
|
|
132
|
+
}): Promise<PreparedPresentation<P> | undefined>;
|
|
133
|
+
export declare function presentPrepared<P, R>(prepared: PreparedPresentation<P>, sink: (payload: P) => R | Promise<R>, control?: PresentationControl): Promise<PresentationReport<P, R>>;
|
|
134
|
+
/** Takes all local custody synchronously, then owns eligibility/effect/settlement/cleanup. */
|
|
135
|
+
export declare function presentPreparedBatch<P, B, R>(prepared: readonly PreparedPresentation<P>[], render: (eligible: readonly PreparedPresentation<P>[]) => {
|
|
136
|
+
readonly payload: B;
|
|
137
|
+
readonly included: readonly PreparedPresentation<P>[];
|
|
138
|
+
}, sink: (payload: B) => R | Promise<R>, control?: PresentationControl): Promise<PresentationReport<P, R>>;
|
|
139
|
+
export {};
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
async function settle(port, claim, completion) {
|
|
2
|
+
try {
|
|
3
|
+
return { completion, persistence: { status: await port.settle(claim, completion) ? 'recorded' : 'not-recorded' } };
|
|
4
|
+
}
|
|
5
|
+
catch (error) {
|
|
6
|
+
return { completion, persistence: { status: 'error', error } };
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
async function current(port, signal) {
|
|
10
|
+
try {
|
|
11
|
+
return await port.current(signal);
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/** Timer bounds the external attempt only, not claim/currentness/persistence. No automatic retry. */
|
|
18
|
+
export async function dispatchEffect(port, send, options) {
|
|
19
|
+
const notInvoked = { kind: 'not-invoked' };
|
|
20
|
+
if (options.signal?.aborted)
|
|
21
|
+
return { observation: notInvoked };
|
|
22
|
+
const claim = await port.claim(options.signal);
|
|
23
|
+
if (claim === undefined)
|
|
24
|
+
return { observation: notInvoked };
|
|
25
|
+
if (!await current(port, options.signal))
|
|
26
|
+
return { observation: notInvoked,
|
|
27
|
+
settlement: await settle(port, claim, { outcome: 'released', reason: 'not-current', phase: 'initial' }) };
|
|
28
|
+
if (options.signal?.aborted)
|
|
29
|
+
return { observation: notInvoked,
|
|
30
|
+
settlement: await settle(port, claim, { outcome: 'released', reason: 'cancelled' }) };
|
|
31
|
+
let denied = false;
|
|
32
|
+
let timer;
|
|
33
|
+
const deadline = Date.now() + options.timeoutMs;
|
|
34
|
+
const timeout = new Promise(resolve => {
|
|
35
|
+
timer = setTimeout(() => resolve({ kind: 'timeout' }), options.timeoutMs);
|
|
36
|
+
});
|
|
37
|
+
let observation;
|
|
38
|
+
let completion;
|
|
39
|
+
try {
|
|
40
|
+
observation = await Promise.race([timeout, (async () => {
|
|
41
|
+
try {
|
|
42
|
+
return { kind: 'returned', result: await send({ claim, deadline, ...options,
|
|
43
|
+
beforeSend: async () => {
|
|
44
|
+
const allowed = await current(port, options.signal);
|
|
45
|
+
if (!allowed)
|
|
46
|
+
denied = true;
|
|
47
|
+
return allowed;
|
|
48
|
+
},
|
|
49
|
+
}) };
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
return { kind: 'threw', error };
|
|
53
|
+
}
|
|
54
|
+
})()]);
|
|
55
|
+
// Freeze before persistence. Late policy results/sender completions cannot rewrite it.
|
|
56
|
+
completion = observation.kind === 'timeout' ? { outcome: 'unknown', reason: 'timeout' }
|
|
57
|
+
: denied ? { outcome: 'released', reason: 'not-current', phase: 'final' }
|
|
58
|
+
: observation.kind === 'threw' ? { outcome: 'unknown', reason: 'threw', error: observation.error }
|
|
59
|
+
: observation.result.outcome === 'not-capable'
|
|
60
|
+
? { outcome: 'released', reason: 'not-capable', data: observation.result.data }
|
|
61
|
+
: { outcome: observation.result.outcome, reason: 'returned', data: observation.result.data };
|
|
62
|
+
}
|
|
63
|
+
finally {
|
|
64
|
+
if (timer !== undefined)
|
|
65
|
+
clearTimeout(timer);
|
|
66
|
+
}
|
|
67
|
+
return { observation, settlement: await settle(port, claim, completion) };
|
|
68
|
+
}
|
|
69
|
+
const preparedBrand = Symbol('prepared presentation');
|
|
70
|
+
const custody = new WeakMap();
|
|
71
|
+
export async function preparePresentation(work, control) {
|
|
72
|
+
if (control?.signal?.aborted)
|
|
73
|
+
return undefined;
|
|
74
|
+
const claim = await work.port.claim(control?.signal);
|
|
75
|
+
if (claim === undefined)
|
|
76
|
+
return undefined;
|
|
77
|
+
let resolve;
|
|
78
|
+
let decided = false;
|
|
79
|
+
const state = {
|
|
80
|
+
owned: false, coverage: work.coverage,
|
|
81
|
+
eligible: signal => signal?.aborted ? Promise.resolve(false) : current(work.port, signal),
|
|
82
|
+
done: new Promise(res => { resolve = res; }),
|
|
83
|
+
finish(completion) {
|
|
84
|
+
if (decided)
|
|
85
|
+
return;
|
|
86
|
+
decided = true;
|
|
87
|
+
// Defer callbacks: every included handle is marked before any settlement starts.
|
|
88
|
+
void Promise.resolve().then(() => settle(work.port, claim, completion)).then(resolve);
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
const prepared = Object.freeze({
|
|
92
|
+
[preparedBrand]: true,
|
|
93
|
+
payload: work.payload,
|
|
94
|
+
eligible: (signal) => state.owned ? Promise.resolve(false) : state.eligible(signal),
|
|
95
|
+
release() {
|
|
96
|
+
if (!state.owned) {
|
|
97
|
+
state.owned = true;
|
|
98
|
+
state.finish({ outcome: 'released', reason: 'unused' });
|
|
99
|
+
}
|
|
100
|
+
return state.done;
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
custody.set(prepared, state);
|
|
104
|
+
return prepared;
|
|
105
|
+
}
|
|
106
|
+
export function presentPrepared(prepared, sink, control) {
|
|
107
|
+
return presentPreparedBatch([prepared], eligible => ({ payload: prepared.payload, included: eligible }), sink, control);
|
|
108
|
+
}
|
|
109
|
+
/** Takes all local custody synchronously, then owns eligibility/effect/settlement/cleanup. */
|
|
110
|
+
export async function presentPreparedBatch(prepared, render, sink, control = {}) {
|
|
111
|
+
const handles = [...prepared];
|
|
112
|
+
const states = handles.map(handle => custody.get(handle));
|
|
113
|
+
if (new Set(handles).size !== handles.length || states.some(state => state === undefined || state.owned)) {
|
|
114
|
+
await Promise.all([...new Set(handles)].filter(handle => custody.has(handle) && !custody.get(handle).owned).map(handle => handle.release()));
|
|
115
|
+
throw new TypeError('Presentation requires unique, unused prepared custody.');
|
|
116
|
+
}
|
|
117
|
+
const owned = states;
|
|
118
|
+
for (const state of owned)
|
|
119
|
+
state.owned = true;
|
|
120
|
+
let observation = { kind: 'not-invoked' };
|
|
121
|
+
try {
|
|
122
|
+
const eligible = [];
|
|
123
|
+
for (const [index, state] of owned.entries()) {
|
|
124
|
+
if (await state.eligible(control.signal))
|
|
125
|
+
eligible.push(handles[index]);
|
|
126
|
+
else
|
|
127
|
+
state.finish({ outcome: 'released', reason: control.signal?.aborted ? 'cancelled' : 'not-current' });
|
|
128
|
+
}
|
|
129
|
+
const rendered = render(Object.freeze(eligible));
|
|
130
|
+
const included = [...rendered.included];
|
|
131
|
+
if (new Set(included).size !== included.length || included.some(handle => !eligible.includes(handle)))
|
|
132
|
+
throw new TypeError('Included presentations must be a unique subset of eligible custody.');
|
|
133
|
+
if (included.length > 0 && !control.signal?.aborted) {
|
|
134
|
+
let completion;
|
|
135
|
+
try {
|
|
136
|
+
const value = await sink(rendered.payload);
|
|
137
|
+
observation = { kind: 'returned', value };
|
|
138
|
+
completion = { outcome: 'presented', reason: 'returned' };
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
let outcome = 'unknown';
|
|
142
|
+
try {
|
|
143
|
+
if (control.classifyError?.(error) === 'failed')
|
|
144
|
+
outcome = 'failed';
|
|
145
|
+
}
|
|
146
|
+
catch { /* conservative */ }
|
|
147
|
+
observation = { kind: 'threw', error, outcome };
|
|
148
|
+
completion = { outcome, reason: 'threw', error };
|
|
149
|
+
}
|
|
150
|
+
for (const handle of included) {
|
|
151
|
+
const state = custody.get(handle);
|
|
152
|
+
state.finish(completion.outcome === 'presented'
|
|
153
|
+
? { outcome: state.coverage === 'full' ? 'presented' : 'clipped', reason: 'returned' } : completion);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
finally {
|
|
158
|
+
for (const state of owned)
|
|
159
|
+
state.finish({ outcome: 'released', reason: control.signal?.aborted ? 'cancelled' : 'unused' });
|
|
160
|
+
await Promise.all(owned.map(state => state.done));
|
|
161
|
+
}
|
|
162
|
+
return { observation, settlements: await Promise.all(handles.map(async (handle, index) => ({ prepared: handle, result: await owned[index].done }))) };
|
|
163
|
+
}
|