@astrosheep/square 0.3.106 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/activity-feed.js +1 -1
- package/dist/artifact-store.d.ts +41 -0
- package/dist/artifact-store.js +272 -0
- package/dist/artifact.d.ts +6 -12
- package/dist/artifact.js +43 -344
- package/dist/attention-presentation.d.ts +13 -7
- package/dist/attention-presentation.js +29 -68
- package/dist/boundary-presentation.d.ts +3 -16
- package/dist/boundary-presentation.js +29 -109
- package/dist/card-presentation.d.ts +1 -0
- package/dist/card-presentation.js +2 -0
- package/dist/catch-decisions.d.ts +2 -2
- package/dist/catch-decisions.js +7 -3
- package/dist/catch-selection.d.ts +8 -0
- package/dist/catch-selection.js +5 -0
- package/dist/claude-delivery.d.ts +14 -12
- package/dist/claude-delivery.js +42 -142
- package/dist/claude-mcp-context.js +2 -2
- package/dist/claude-mod.js +28 -140
- package/dist/claude-transport.d.ts +5 -0
- package/dist/claude-transport.js +34 -0
- package/dist/cli/context.d.ts +2 -2
- package/dist/cli/context.js +7 -10
- package/dist/cli/maintenance-commands.d.ts +0 -4
- package/dist/cli/maintenance-commands.js +2 -29
- package/dist/cli/observation-commands.d.ts +2 -0
- package/dist/cli/observation-commands.js +12 -2
- package/dist/cli/program.js +1 -1
- package/dist/cli/registry.js +4 -3
- package/dist/codex-address.d.ts +2 -2
- package/dist/codex-boundary-state.d.ts +1 -5
- package/dist/codex-boundary-state.js +0 -4
- package/dist/codex-hook.d.ts +7 -10
- package/dist/codex-hook.js +69 -33
- package/dist/codex-queue.d.ts +10 -11
- package/dist/codex-queue.js +32 -34
- package/dist/codex-watch.d.ts +2 -0
- package/dist/codex-watch.js +100 -0
- package/dist/decisions.js +3 -3
- package/dist/delivery-deferral.d.ts +1 -19
- package/dist/delivery-deferral.js +1 -29
- package/dist/delivery-frame.d.ts +7 -0
- package/dist/delivery-frame.js +11 -0
- package/dist/delivery-ledger-sqlite.d.ts +97 -0
- package/dist/delivery-ledger-sqlite.js +496 -0
- package/dist/delivery-ledger.d.ts +196 -0
- package/dist/delivery-ledger.js +268 -0
- package/dist/delivery-lifetime.d.ts +6 -0
- package/dist/delivery-lifetime.js +45 -0
- package/dist/harness-links.js +61 -7
- package/dist/harness.js +12 -17
- package/dist/help.js +3 -6
- package/dist/http-delivery.d.ts +5 -9
- package/dist/http-delivery.js +20 -79
- package/dist/inbox.d.ts +6 -57
- package/dist/inbox.js +24 -380
- package/dist/index.d.ts +7 -4
- package/dist/index.js +5 -2
- package/dist/mcp.d.ts +2 -1
- package/dist/mcp.js +40 -70
- package/dist/message.d.ts +7 -0
- package/dist/message.js +23 -0
- package/dist/model.d.ts +15 -91
- package/dist/model.js +2 -140
- package/dist/neutral.d.ts +72 -0
- package/dist/neutral.js +27 -0
- package/dist/notifications.d.ts +24 -26
- package/dist/notifications.js +39 -143
- package/dist/operation-control.d.ts +4 -0
- package/dist/operation-control.js +20 -0
- package/dist/operation-presentation.d.ts +1 -2
- package/dist/operation-presentation.js +1 -7
- package/dist/owner.d.ts +2 -2
- package/dist/owner.js +1 -1
- package/dist/packages/agent-delivery/src/claude-native.d.ts +2 -0
- package/dist/packages/agent-delivery/src/claude-native.js +5 -0
- package/dist/packages/agent-delivery/src/codex-boundary.d.ts +11 -6
- package/dist/packages/agent-delivery/src/codex-boundary.js +31 -40
- package/dist/packages/agent-delivery/src/codex-hook.d.ts +17 -12
- package/dist/packages/agent-delivery/src/codex-hook.js +26 -39
- package/dist/packages/agent-delivery/src/codex.d.ts +3 -0
- package/dist/packages/agent-delivery/src/codex.js +41 -4
- package/dist/packages/agent-delivery/src/index.d.ts +3 -1
- package/dist/packages/agent-delivery/src/index.js +1 -1
- package/dist/packages/agent-delivery/src/paseo.js +11 -2
- package/dist/packages/agent-delivery/src/types.d.ts +1 -0
- package/dist/paseo-delivery.d.ts +5 -5
- package/dist/paseo-delivery.js +19 -99
- package/dist/paseo.d.ts +1 -1
- package/dist/paseo.js +1 -1
- package/dist/plugins/opencode/index.d.ts +5 -3
- package/dist/plugins/opencode/index.js +99 -21
- package/dist/plugins/opencode-shared/delivery.d.ts +23 -0
- package/dist/plugins/opencode-shared/delivery.js +109 -0
- package/dist/plugins/opencode-v1/index.d.ts +28 -0
- package/dist/plugins/opencode-v1/index.js +41 -0
- package/dist/plugins/opencode-v1/runtime.d.ts +56 -0
- package/dist/plugins/opencode-v1/runtime.js +152 -0
- package/dist/plugins/opencode-v1/transport.d.ts +13 -0
- package/dist/plugins/opencode-v1/transport.js +50 -0
- package/dist/plugins/paseo/shared/card-presentation.d.ts +10 -0
- package/dist/plugins/paseo/shared/card-presentation.js +7 -0
- package/dist/ports.d.ts +33 -214
- package/dist/presentation.d.ts +5 -5
- package/dist/presentation.js +1 -0
- package/dist/runtime.js +2 -2
- package/dist/search.js +1 -1
- package/dist/session-lifecycle.d.ts +8 -7
- package/dist/session-lifecycle.js +40 -42
- package/dist/sqlite-framing.d.ts +8 -0
- package/dist/sqlite-framing.js +31 -0
- package/dist/sqlite-square.d.ts +16 -26
- package/dist/sqlite-square.js +199 -262
- package/dist/sqlite-values.d.ts +21 -0
- package/dist/sqlite-values.js +78 -0
- package/dist/square-api.d.ts +1 -1
- package/dist/square-api.js +5 -8
- package/dist/square-core.d.ts +0 -1
- package/dist/square-domain.d.ts +12 -10
- package/dist/square-domain.js +105 -223
- package/dist/square-facade.d.ts +25 -76
- package/dist/square-location.d.ts +6 -1
- package/dist/square-location.js +1 -1
- package/dist/square-post.d.ts +53 -0
- package/dist/square-post.js +4 -0
- package/dist/square-posting.d.ts +13 -0
- package/dist/square-posting.js +174 -0
- package/dist/square-producer-sqlite.d.ts +11 -0
- package/dist/square-producer-sqlite.js +210 -0
- package/dist/square-receive.d.ts +2 -3
- package/dist/square-sqlite.d.ts +33 -42
- package/dist/square-sqlite.js +236 -768
- package/dist/square-tools.d.ts +8 -0
- package/dist/square-tools.js +39 -0
- package/dist/square-values.d.ts +27 -0
- package/dist/square-values.js +90 -0
- package/dist/views.d.ts +6 -6
- package/dist/views.js +14 -9
- package/dist/wake-sink.d.ts +4 -11
- package/dist/wake-sink.js +8 -32
- package/dist/wake-transports.d.ts +4 -0
- package/dist/wake-transports.js +11 -0
- package/examples/symmetric-domains.mjs +29 -249
- package/guides/change-waits.md +13 -53
- package/guides/claude-native.md +19 -172
- package/guides/codex-delivery.md +55 -78
- package/guides/delivery-primitives.md +48 -207
- package/guides/operation-cancellation.md +2 -5
- package/guides/participant-ownership.md +25 -89
- package/guides/pi-native-delivery.md +19 -56
- package/guides/pi-tools.md +6 -8
- package/guides/row-runtime-delivery-evidence.md +2 -0
- package/guides/row-storage-design.md +19 -44
- package/guides/square-api.md +113 -538
- package/package.json +8 -8
- package/plugins/claude/.claude-plugin/plugin.json +1 -1
- package/plugins/claude/hooks/hooks.json +1 -1
- package/plugins/claude/{hooks/register.js → mods/square.js} +4 -7
- package/plugins/claude/types.d.ts +1 -1
- package/plugins/codex/.codex-plugin/plugin.json +1 -1
- package/plugins/codex/hooks/hooks.json +11 -23
- package/plugins/opencode/index.ts +91 -23
- package/plugins/opencode-shared/delivery.ts +91 -0
- package/plugins/opencode-v1/index.ts +36 -0
- package/plugins/opencode-v1/runtime.ts +134 -0
- package/plugins/opencode-v1/transport.ts +41 -0
- package/plugins/paseo/OVERVIEW.md +5 -5
- package/plugins/paseo/README.md +7 -6
- package/plugins/paseo/client/markdown.tsx +5 -5
- package/plugins/paseo/client/square-message.tsx +28 -24
- package/plugins/paseo/client/transform.ts +8 -5
- package/plugins/paseo/shared/card-presentation.ts +16 -0
- package/plugins/paseo/shared/message.ts +6 -11
- package/plugins/pi/square-pi.js +167 -451
- package/dist/channel.d.ts +0 -74
- package/dist/channel.js +0 -92
- package/dist/claude-delivery-status.d.ts +0 -13
- package/dist/claude-delivery-status.js +0 -46
- package/dist/cmd/notify-once.d.ts +0 -2
- package/dist/cmd/notify-once.js +0 -29
- package/dist/delivery-attempts.d.ts +0 -52
- package/dist/delivery-attempts.js +0 -73
- package/dist/delivery-effects.d.ts +0 -139
- package/dist/delivery-effects.js +0 -163
- package/dist/delivery-health.d.ts +0 -51
- package/dist/delivery-health.js +0 -173
- package/dist/delivery-operations.d.ts +0 -9
- package/dist/delivery-operations.js +0 -168
- package/dist/delivery-primitives.d.ts +0 -5
- package/dist/delivery-primitives.js +0 -3
- package/dist/delivery.d.ts +0 -45
- package/dist/delivery.js +0 -106
- package/dist/plugins/opencode/delivery.d.ts +0 -19
- package/dist/plugins/opencode/delivery.js +0 -238
- package/dist/presentation-operations.d.ts +0 -18
- package/dist/presentation-operations.js +0 -86
- package/dist/square-index.d.ts +0 -16
- package/dist/square-index.js +0 -106
- package/dist/square-legacy-framing.d.ts +0 -11
- package/dist/square-legacy-framing.js +0 -64
- package/dist/square-migration-host-ledger.d.ts +0 -26
- package/dist/square-migration-host-ledger.js +0 -169
- package/dist/square-migration.d.ts +0 -30
- package/dist/square-migration.js +0 -599
- package/guides/migration.md +0 -156
- package/plugins/opencode/delivery.ts +0 -211
package/dist/square-facade.d.ts
CHANGED
|
@@ -1,12 +1,27 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
export type {
|
|
1
|
+
import type { MetadataMatch } from './delivery-deferral.js';
|
|
2
|
+
export type { MetadataMatch, MetadataScalar } from './delivery-deferral.js';
|
|
3
3
|
import type { ActivityId, Perception, Reach } from './square-core.js';
|
|
4
|
-
import type { Owner,
|
|
5
|
-
import type {
|
|
4
|
+
import type { Owner, HydratedAct, WakeAddress, ActivityMetadata, DirectedNotificationRoute } from './model.js';
|
|
5
|
+
import type { SquareResourceOwner } from './ports.js';
|
|
6
|
+
import type { OperationControl } from './neutral.js';
|
|
7
|
+
export interface ActivitySearch {
|
|
8
|
+
readonly text: string;
|
|
9
|
+
readonly mode: 'literal' | 'regex';
|
|
10
|
+
}
|
|
11
|
+
export interface AckResult {
|
|
12
|
+
readonly consumed: number;
|
|
13
|
+
}
|
|
14
|
+
import type { FutureAck } from './square-post.js';
|
|
15
|
+
export type { NotificationId, SendInput, SendResult, FutureAck } from './square-post.js';
|
|
16
|
+
export interface HeldSeat {
|
|
17
|
+
readonly name: string;
|
|
18
|
+
readonly generation: string;
|
|
19
|
+
}
|
|
20
|
+
export type { CatchWaitStart, OperationProgress, OperationControl, OperationFailureDetail, OperationError } from './neutral.js';
|
|
6
21
|
export type { ActivityId, Perception, Reach } from './square-core.js';
|
|
7
|
-
export type { Owner, WakeAddress, ActivityMetadata,
|
|
8
|
-
/** Indexed archive event, including read bookkeeping. */
|
|
9
|
-
export type ArchiveRecord = Readonly<
|
|
22
|
+
export type { Owner, WakeAddress, ActivityMetadata, JsonValue, DirectedNotificationRoute } from './model.js';
|
|
23
|
+
/** Indexed archive event, including read bookkeeping and a say's linked content. */
|
|
24
|
+
export type ArchiveRecord = Readonly<HydratedAct>;
|
|
10
25
|
export interface Activity {
|
|
11
26
|
readonly id: ActivityId;
|
|
12
27
|
readonly at: number;
|
|
@@ -14,7 +29,6 @@ export interface Activity {
|
|
|
14
29
|
readonly actor: string;
|
|
15
30
|
readonly body?: string;
|
|
16
31
|
readonly metadata?: ActivityMetadata;
|
|
17
|
-
readonly delivery?: ActivityDelivery;
|
|
18
32
|
readonly mentions: readonly string[];
|
|
19
33
|
readonly reach?: Reach;
|
|
20
34
|
readonly target?: string;
|
|
@@ -50,22 +64,6 @@ export interface ExpressResult {
|
|
|
50
64
|
readonly hint?: string;
|
|
51
65
|
readonly observation?: ExpressObservation;
|
|
52
66
|
}
|
|
53
|
-
export type ChannelTruncate = number | false;
|
|
54
|
-
export interface ChannelSendInput {
|
|
55
|
-
readonly from: string;
|
|
56
|
-
readonly to: string | readonly string[];
|
|
57
|
-
readonly body: string;
|
|
58
|
-
readonly truncate?: ChannelTruncate;
|
|
59
|
-
readonly metadata?: ActivityMetadata;
|
|
60
|
-
}
|
|
61
|
-
export interface ChannelSendResult {
|
|
62
|
-
readonly id: ActivityId;
|
|
63
|
-
readonly at: number;
|
|
64
|
-
readonly from: string;
|
|
65
|
-
readonly to: readonly string[];
|
|
66
|
-
readonly body: string;
|
|
67
|
-
readonly metadata?: ActivityMetadata;
|
|
68
|
-
}
|
|
69
67
|
export interface ListenerChangeResult {
|
|
70
68
|
readonly activity: Activity | null;
|
|
71
69
|
readonly observation?: ListenerObservation;
|
|
@@ -86,10 +84,6 @@ export interface CatchOptions extends ObservationOptions {
|
|
|
86
84
|
readonly limit?: number;
|
|
87
85
|
readonly replace?: boolean;
|
|
88
86
|
}
|
|
89
|
-
export interface ActivitySearch {
|
|
90
|
-
readonly text: string;
|
|
91
|
-
readonly mode: 'literal' | 'regex';
|
|
92
|
-
}
|
|
93
87
|
export interface ActivityCursor {
|
|
94
88
|
readonly id: ActivityId;
|
|
95
89
|
readonly direction: 'before' | 'after';
|
|
@@ -111,10 +105,6 @@ export interface PendingResult {
|
|
|
111
105
|
/** Matching unconsumed activities outside this page. */
|
|
112
106
|
readonly remaining: number;
|
|
113
107
|
}
|
|
114
|
-
export interface AckResult {
|
|
115
|
-
/** Newly consumed addressed IDs; repeated acknowledgement is a no-op. */
|
|
116
|
-
readonly consumed: number;
|
|
117
|
-
}
|
|
118
108
|
export type CatchReason = 'drained' | 'idle' | 'capped' | 'quorum' | 'replaced' | 'already-active';
|
|
119
109
|
export interface CatchResult {
|
|
120
110
|
readonly activities: readonly PerceivedActivity[];
|
|
@@ -124,37 +114,6 @@ export interface CatchResult {
|
|
|
124
114
|
readonly hint?: string;
|
|
125
115
|
readonly observation?: CatchObservation;
|
|
126
116
|
}
|
|
127
|
-
export interface CatchWaitStart {
|
|
128
|
-
readonly idleMs: number;
|
|
129
|
-
readonly replacementRequested: boolean;
|
|
130
|
-
readonly replaced: boolean;
|
|
131
|
-
}
|
|
132
|
-
export type OperationProgress = {
|
|
133
|
-
readonly kind: 'waiting';
|
|
134
|
-
readonly reason: 'throttled';
|
|
135
|
-
readonly delayMs?: number;
|
|
136
|
-
} | ({
|
|
137
|
-
readonly kind: 'catch-started';
|
|
138
|
-
readonly participant: string;
|
|
139
|
-
} & CatchWaitStart);
|
|
140
|
-
export interface OperationControl {
|
|
141
|
-
readonly signal?: AbortSignal;
|
|
142
|
-
readonly onProgress?: (progress: OperationProgress) => void;
|
|
143
|
-
}
|
|
144
|
-
export interface OperationFailureDetail {
|
|
145
|
-
readonly operation: 'express' | 'join' | 'catch' | 'history' | 'other';
|
|
146
|
-
readonly subject?: 'caller' | 'filter' | 'recipient';
|
|
147
|
-
readonly participant?: string;
|
|
148
|
-
readonly expressEffect?: 'unsent' | 'landed' | 'unknown';
|
|
149
|
-
readonly landedId?: ActivityId;
|
|
150
|
-
readonly historyBounds?: {
|
|
151
|
-
readonly maxLimit: number;
|
|
152
|
-
readonly totalMatches: number;
|
|
153
|
-
};
|
|
154
|
-
}
|
|
155
|
-
export interface OperationError extends Error {
|
|
156
|
-
readonly operation?: OperationFailureDetail;
|
|
157
|
-
}
|
|
158
117
|
export interface HistoryQuery extends ActivityQuery, ObservationOptions {
|
|
159
118
|
/** Ten by default; null explicitly reads the full archive. */
|
|
160
119
|
readonly limit?: number | null;
|
|
@@ -170,7 +129,6 @@ export interface HistoryResult {
|
|
|
170
129
|
}
|
|
171
130
|
export interface OpenOptions {
|
|
172
131
|
readonly clock?: () => number;
|
|
173
|
-
readonly wakeTransport?: WakeTransportPort | false;
|
|
174
132
|
}
|
|
175
133
|
export interface SquareLocation {
|
|
176
134
|
readonly path?: string;
|
|
@@ -202,7 +160,7 @@ export interface JoinResult {
|
|
|
202
160
|
}
|
|
203
161
|
export interface Participant {
|
|
204
162
|
readonly name: string;
|
|
205
|
-
readonly generation:
|
|
163
|
+
readonly generation: string;
|
|
206
164
|
express(body: string | undefined, options?: ExpressOptions, control?: OperationControl): Promise<ExpressResult>;
|
|
207
165
|
express(input: ExpressInput, control?: OperationControl): Promise<ExpressResult>;
|
|
208
166
|
listen(target: string, options?: ObservationOptions, control?: OperationControl): Promise<ListenerChangeResult>;
|
|
@@ -211,20 +169,11 @@ export interface Participant {
|
|
|
211
169
|
listening(options?: ObservationOptions, control?: OperationControl): Promise<ListeningResult>;
|
|
212
170
|
catch(options?: CatchOptions, control?: OperationControl): Promise<CatchResult>;
|
|
213
171
|
pending(options?: PendingOptions, control?: OperationControl): Promise<PendingResult>;
|
|
172
|
+
/** Local custody only: ledger-only detach/delete is observed on privileged reconciliation. */
|
|
214
173
|
ack(ids: readonly ActivityId[], control?: OperationControl): Promise<AckResult>;
|
|
174
|
+
ack(input: FutureAck, control?: OperationControl): Promise<AckResult>;
|
|
215
175
|
/** Leave without a message. Only an omitted or empty body is accepted. */
|
|
216
176
|
done(body?: string, options?: ObservationOptions, control?: OperationControl): Promise<LifecycleResult>;
|
|
217
|
-
deferDelivery(input: Readonly<{
|
|
218
|
-
match: MetadataMatch;
|
|
219
|
-
durationMs: number;
|
|
220
|
-
}>, control?: OperationControl): Promise<DeliveryDeferral>;
|
|
221
|
-
renewDeliveryDeferral(id: string, durationMs: number, control?: OperationControl): Promise<DeliveryDeferral | null>;
|
|
222
|
-
releaseDeliveryDeferral(id: string, control?: OperationControl): Promise<boolean>;
|
|
223
|
-
}
|
|
224
|
-
export interface HeldSeat {
|
|
225
|
-
readonly name: string;
|
|
226
|
-
readonly generation: ActivityId;
|
|
227
|
-
readonly attachment: 'attached' | 'detached';
|
|
228
177
|
}
|
|
229
178
|
export type ParticipantPresence = 'never-joined' | 'active' | 'watching' | 'away' | 'done';
|
|
230
179
|
export interface ParticipantSummary {
|
package/dist/square-location.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
-
import { SquareError } from './
|
|
2
|
+
import { SquareError } from './neutral.js';
|
|
3
3
|
export function squareLocation(input) {
|
|
4
4
|
const selected = input.path ?? (input.env ?? process.env).SQUARE_LOCATION;
|
|
5
5
|
if (selected === undefined || selected.trim() === '')
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { type ActivityMetadata, type DirectedNotificationRoute, type Owner } from './model.js';
|
|
2
|
+
import type { MetadataMatch } from './delivery-deferral.js';
|
|
3
|
+
import type { SeatRef } from './ports.js';
|
|
4
|
+
/** Producer notices are not activities in the square. */
|
|
5
|
+
export type NotificationId = `notice/${number}`;
|
|
6
|
+
export interface SendInput {
|
|
7
|
+
readonly from: string;
|
|
8
|
+
readonly to: string | readonly string[];
|
|
9
|
+
readonly body: string;
|
|
10
|
+
readonly metadata?: ActivityMetadata;
|
|
11
|
+
}
|
|
12
|
+
export interface SendResult {
|
|
13
|
+
readonly id: NotificationId;
|
|
14
|
+
readonly at: number;
|
|
15
|
+
readonly from: string;
|
|
16
|
+
readonly to: readonly string[];
|
|
17
|
+
readonly body: string;
|
|
18
|
+
readonly metadata?: ActivityMetadata;
|
|
19
|
+
}
|
|
20
|
+
/** The caller supplies every scalar equality identifying one exact outcome. */
|
|
21
|
+
export interface FutureAck {
|
|
22
|
+
readonly from: string;
|
|
23
|
+
readonly match: MetadataMatch;
|
|
24
|
+
readonly future: 'exact';
|
|
25
|
+
}
|
|
26
|
+
/** One Square-owned recipient card; neither an endpoint nor a shared message resource. */
|
|
27
|
+
export interface SquarePost {
|
|
28
|
+
readonly kind: 'activity' | 'notice';
|
|
29
|
+
readonly index: number;
|
|
30
|
+
readonly recipient: string;
|
|
31
|
+
readonly owner: Owner;
|
|
32
|
+
readonly generation: string;
|
|
33
|
+
readonly from: string;
|
|
34
|
+
readonly at: number;
|
|
35
|
+
readonly body?: string;
|
|
36
|
+
readonly metadata?: ActivityMetadata;
|
|
37
|
+
readonly route?: DirectedNotificationRoute;
|
|
38
|
+
readonly full?: boolean;
|
|
39
|
+
readonly consumed?: boolean;
|
|
40
|
+
}
|
|
41
|
+
export interface ProducerRead {
|
|
42
|
+
pendingPosts(): readonly SquarePost[];
|
|
43
|
+
postedPosts(): readonly SquarePost[];
|
|
44
|
+
ownsPost(token: string, exportAt?: number): boolean;
|
|
45
|
+
}
|
|
46
|
+
export interface ProducerWrite {
|
|
47
|
+
insertNotice(input: SendInput, at: number): SendResult;
|
|
48
|
+
ackMatching(seat: SeatRef, input: FutureAck, at: number): number;
|
|
49
|
+
acquirePost(post: SquarePost, token: string, pid: number, now: number, exportAt?: number): false | 'initial' | 'recovered';
|
|
50
|
+
settlePost(token: string, full: boolean, disposition: 'posted' | 'uncertain' | 'never-exported'): boolean;
|
|
51
|
+
foldReceipt(post: SquarePost, at: number): boolean;
|
|
52
|
+
}
|
|
53
|
+
export declare function squarePostKey(location: string, post: Pick<SquarePost, 'kind' | 'index' | 'recipient' | 'generation'>): string;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { nameKey } from './model.js';
|
|
2
|
+
export function squarePostKey(location, post) {
|
|
3
|
+
return `square:${location}#${post.kind === 'activity' ? 'act' : 'notice'}/${post.index}/recipient/${encodeURIComponent(nameKey(post.recipient))}/join/${encodeURIComponent(post.generation)}`;
|
|
4
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { DeliveryLedger, type DeliveryRoute, type DeliverySession } from './delivery-ledger.js';
|
|
2
|
+
import { type Seat } from './model.js';
|
|
3
|
+
import type { SquareArtifactPort } from './ports.js';
|
|
4
|
+
/** Local producer custody and current host attachment are different observations. */
|
|
5
|
+
export declare function deliverySessionsForSeats(ledger: DeliveryLedger, seats: readonly Seat[]): Promise<ReadonlyMap<string, DeliverySession | undefined>>;
|
|
6
|
+
/** Read-only host observations for status; callers fence them against the captured World seats. */
|
|
7
|
+
export declare function readSquareSessions(seats: readonly Seat[], env: NodeJS.ProcessEnv): Promise<ReadonlyMap<string, DeliverySession | undefined>>;
|
|
8
|
+
/** A first producer join may register an absent owner, never resume or revive one. */
|
|
9
|
+
export declare function ensureInitialDeliverySession(owner: Seat['owner'], env: NodeJS.ProcessEnv, route?: DeliveryRoute): Promise<boolean>;
|
|
10
|
+
/** Called only by privileged mutations. Sandbox lease/ACK/selection never enters here. */
|
|
11
|
+
export declare function reconcileSquareDelivery(artifact: SquareArtifactPort, location: string, env: NodeJS.ProcessEnv, clock: () => number): Promise<void>;
|
|
12
|
+
/** One export path for Square activities and Square-owned notices. No transaction spans I/O. */
|
|
13
|
+
export declare function flushSquarePosts(artifact: SquareArtifactPort, location: string, env: NodeJS.ProcessEnv, clock: () => number): Promise<void>;
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { DeliveryLedger, deliveryLedgerLocation } from './delivery-ledger.js';
|
|
3
|
+
import { dispatchPostedCard } from './notifications.js';
|
|
4
|
+
import { retainDeliveryLedger } from './delivery-lifetime.js';
|
|
5
|
+
import { projectNotification, displayAttentionPath } from './attention-presentation.js';
|
|
6
|
+
import { coreDone } from './decisions.js';
|
|
7
|
+
import { activityMetadataHasContent } from './model.js';
|
|
8
|
+
import { formatActivityId } from './square-core.js';
|
|
9
|
+
import { squarePostKey } from './square-post.js';
|
|
10
|
+
const openLedger = (env) => DeliveryLedger.open({
|
|
11
|
+
location: deliveryLedgerLocation(env),
|
|
12
|
+
});
|
|
13
|
+
function renderPost(location, post) {
|
|
14
|
+
if (post.kind === 'notice')
|
|
15
|
+
return { text: post.body ?? '', full: true };
|
|
16
|
+
const projected = projectNotification({ squarePath: location, actIndex: post.index, recipient: post.recipient,
|
|
17
|
+
actor: post.from, route: post.route, body: post.body ?? '',
|
|
18
|
+
metadataOnly: !(post.body?.trim()) && activityMetadataHasContent(post.metadata) });
|
|
19
|
+
return { text: projected.text, full: projected.isFull };
|
|
20
|
+
}
|
|
21
|
+
/** Local producer custody and current host attachment are different observations. */
|
|
22
|
+
export async function deliverySessionsForSeats(ledger, seats) {
|
|
23
|
+
const owners = [...new Set(seats.map(seat => seat.owner))];
|
|
24
|
+
return new Map(await Promise.all(owners.map(async (owner) => [owner, await ledger.session(owner)])));
|
|
25
|
+
}
|
|
26
|
+
/** Read-only host observations for status; callers fence them against the captured World seats. */
|
|
27
|
+
export async function readSquareSessions(seats, env) {
|
|
28
|
+
if (!seats.length)
|
|
29
|
+
return new Map();
|
|
30
|
+
const ledger = await openLedger(env);
|
|
31
|
+
try {
|
|
32
|
+
return await deliverySessionsForSeats(ledger, seats);
|
|
33
|
+
}
|
|
34
|
+
finally {
|
|
35
|
+
await ledger.close();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/** A first producer join may register an absent owner, never resume or revive one. */
|
|
39
|
+
export async function ensureInitialDeliverySession(owner, env, route) {
|
|
40
|
+
const ledger = await openLedger(env);
|
|
41
|
+
try {
|
|
42
|
+
const current = await ledger.session(owner);
|
|
43
|
+
if (!current) {
|
|
44
|
+
return !!await ledger.attach({ owner, expectedGeneration: null, routes: route ? [route] : [] });
|
|
45
|
+
}
|
|
46
|
+
if (current.state !== 'attached')
|
|
47
|
+
return false;
|
|
48
|
+
if (route)
|
|
49
|
+
return ledger.updateRoutes({ owner, generation: current.generation,
|
|
50
|
+
routes: [...current.routes.filter(candidate => candidate.kind !== route.kind), route] });
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
finally {
|
|
54
|
+
await ledger.close();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/** Called only by privileged mutations. Sandbox lease/ACK/selection never enters here. */
|
|
58
|
+
export async function reconcileSquareDelivery(artifact, location, env, clock) {
|
|
59
|
+
const snapshot = (await artifact.read(facts => ({ seats: facts.roster().filter(person => person.joined)
|
|
60
|
+
.flatMap(person => { const seat = facts.seat(person.name); return seat ? [seat] : []; }), posts: facts.postedPosts() }))).value;
|
|
61
|
+
if (!snapshot.seats.length && !snapshot.posts.length)
|
|
62
|
+
return;
|
|
63
|
+
const ledger = await openLedger(env);
|
|
64
|
+
try {
|
|
65
|
+
const sessions = await deliverySessionsForSeats(ledger, snapshot.seats);
|
|
66
|
+
for (const seat of snapshot.seats) {
|
|
67
|
+
const session = sessions.get(seat.owner);
|
|
68
|
+
if (!session)
|
|
69
|
+
continue;
|
|
70
|
+
if (session.state === 'deleted') {
|
|
71
|
+
await artifact.transactAs({ kind: 'host-cleanup', seat }, facts => {
|
|
72
|
+
const current = facts.seat(seat.participant);
|
|
73
|
+
if (current?.owner === seat.owner && current.generation === seat.generation && facts.participant(seat.participant)?.joined)
|
|
74
|
+
facts.land(coreDone(facts, seat.participant, '', clock()));
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
else if (seat.attachment !== session.state) {
|
|
78
|
+
const attachment = session.state;
|
|
79
|
+
await artifact.transactAs({ kind: 'producer' }, facts => facts.setSeatAttachment(seat, attachment));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
// A full receipt is monotonic external presentation evidence, never inferred from silence.
|
|
83
|
+
const receipts = new Map();
|
|
84
|
+
for (const owner of new Set(snapshot.posts.map(post => post.owner))) {
|
|
85
|
+
const keys = snapshot.posts.filter(post => post.owner === owner).map(post => squarePostKey(location, post));
|
|
86
|
+
receipts.set(owner, new Map((await ledger.receipts(owner, { keys })).map(receipt => [receipt.key, receipt.at])));
|
|
87
|
+
}
|
|
88
|
+
for (const post of snapshot.posts) {
|
|
89
|
+
const cardKey = squarePostKey(location, post);
|
|
90
|
+
const receipt = receipts.get(post.owner)?.get(cardKey);
|
|
91
|
+
if (receipt !== undefined && post.full === true && !post.consumed)
|
|
92
|
+
await artifact.transactAs({ kind: 'producer' }, facts => facts.foldReceipt(post, receipt));
|
|
93
|
+
}
|
|
94
|
+
await suppressWithdrawn(artifact, ledger, location);
|
|
95
|
+
}
|
|
96
|
+
finally {
|
|
97
|
+
await ledger.close();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
async function suppressWithdrawn(artifact, ledger, location) {
|
|
101
|
+
const posts = (await artifact.read(facts => facts.postedPosts().filter(post => {
|
|
102
|
+
const seat = facts.seat(post.recipient);
|
|
103
|
+
return post.consumed || !seat || seat.owner !== post.owner || seat.generation !== post.generation;
|
|
104
|
+
}))).value;
|
|
105
|
+
// Departure withdraws only the captured notification, never producer read history or held effects.
|
|
106
|
+
for (const post of posts)
|
|
107
|
+
await ledger.suppress({ owner: post.owner, key: squarePostKey(location, post), scope: 'exact' });
|
|
108
|
+
}
|
|
109
|
+
/** One export path for Square activities and Square-owned notices. No transaction spans I/O. */
|
|
110
|
+
export async function flushSquarePosts(artifact, location, env, clock) {
|
|
111
|
+
// Delivery enrichment cannot turn a committed producer mutation into a retry of that mutation.
|
|
112
|
+
let lifetime;
|
|
113
|
+
try {
|
|
114
|
+
const snapshot = (await artifact.read(facts => ({ pending: facts.pendingPosts(), posted: facts.postedPosts() }))).value;
|
|
115
|
+
if (!snapshot.pending.length && !snapshot.posted.length)
|
|
116
|
+
return;
|
|
117
|
+
const ledger = await openLedger(env);
|
|
118
|
+
lifetime = retainDeliveryLedger(ledger);
|
|
119
|
+
for (const post of snapshot.pending) {
|
|
120
|
+
const session = await ledger.session(post.owner);
|
|
121
|
+
if (session?.state !== 'attached')
|
|
122
|
+
continue;
|
|
123
|
+
const rendered = renderPost(location, post), token = randomUUID();
|
|
124
|
+
const owned = (await artifact.transactAs({ kind: 'producer' }, facts => facts.acquirePost({ ...post, full: rendered.full }, token, process.pid, clock(), Date.now()))).value;
|
|
125
|
+
if (!owned)
|
|
126
|
+
continue;
|
|
127
|
+
let posted = false, knownUnsent = true;
|
|
128
|
+
try {
|
|
129
|
+
// This check is not custody across the await; the ledger independently fences delivery.
|
|
130
|
+
const current = await ledger.session(post.owner);
|
|
131
|
+
if (current?.state !== 'attached' || current.generation !== session.generation
|
|
132
|
+
|| !(await artifact.read(facts => facts.ownsPost(token, Date.now()))).value)
|
|
133
|
+
continue;
|
|
134
|
+
knownUnsent = false;
|
|
135
|
+
const result = await ledger.post({ owner: post.owner, key: squarePostKey(location, post), text: rendered.text,
|
|
136
|
+
...(post.kind === 'activity' ? { details: { kind: 'presentation', origin: displayAttentionPath(location),
|
|
137
|
+
ref: formatActivityId(post.index), author: post.from, body: post.body ?? '',
|
|
138
|
+
...(post.route === 'bell' ? { badge: '🔔' } : {}) } } : {}),
|
|
139
|
+
});
|
|
140
|
+
posted = ['posted', 'duplicate', 'suppressed'].includes(result.status);
|
|
141
|
+
knownUnsent = !posted;
|
|
142
|
+
if (result.status === 'conflict')
|
|
143
|
+
throw new Error('A Square card key already holds different text.');
|
|
144
|
+
}
|
|
145
|
+
finally {
|
|
146
|
+
const disposition = posted ? 'posted' : owned === 'initial' && knownUnsent ? 'never-exported' : 'uncertain';
|
|
147
|
+
await artifact.settleAs({ kind: 'producer' }, facts => facts.settlePost(token, rendered.full, disposition));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
await suppressWithdrawn(artifact, ledger, location);
|
|
151
|
+
const posts = (await artifact.read(facts => facts.postedPosts().filter(post => {
|
|
152
|
+
const seat = facts.seat(post.recipient);
|
|
153
|
+
return !post.consumed && seat?.owner === post.owner && seat.generation === post.generation;
|
|
154
|
+
}))).value;
|
|
155
|
+
for (const owner of new Set(posts.map(post => post.owner))) {
|
|
156
|
+
const session = await ledger.session(owner);
|
|
157
|
+
if (session?.state !== 'attached')
|
|
158
|
+
continue;
|
|
159
|
+
const pendingKeys = new Set((await ledger.pending(owner, { generation: session.generation })).map(card => card.key));
|
|
160
|
+
for (const post of posts.filter(post => post.owner === owner)) {
|
|
161
|
+
const key = squarePostKey(location, post);
|
|
162
|
+
if (pendingKeys.has(key))
|
|
163
|
+
await dispatchPostedCard(ledger, owner, key, lifetime.track, env);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
// Unsettled/uncertain producer work is retried with its stable key at the next mutation.
|
|
169
|
+
// Missing ledger evidence never proves that an external card was not inserted or delivered.
|
|
170
|
+
}
|
|
171
|
+
finally {
|
|
172
|
+
await lifetime?.close();
|
|
173
|
+
}
|
|
174
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { DatabaseSync } from 'node:sqlite';
|
|
2
|
+
import type { ProducerRead, ProducerWrite } from './square-post.js';
|
|
3
|
+
/** Private Square producer bookkeeping, on the artifact's existing connection/transaction. */
|
|
4
|
+
export declare const producerSchemaSql = "CREATE TABLE square_notices (\n notice_index INTEGER PRIMARY KEY CHECK (notice_index >= 0), at REAL NOT NULL,\n sender_json TEXT NOT NULL, body_json TEXT NOT NULL, metadata_json TEXT\n);\nCREATE TABLE notice_audience (\n notice_index INTEGER NOT NULL, recipient_key_json TEXT NOT NULL, recipient_json TEXT NOT NULL,\n owner_json TEXT NOT NULL, generation_json TEXT NOT NULL, consumed_at REAL,\n PRIMARY KEY (notice_index,recipient_key_json)\n) WITHOUT ROWID;\nCREATE TABLE producer_consumption (\n recipient_key_json TEXT NOT NULL, owner_json TEXT NOT NULL, generation_json TEXT NOT NULL,\n sender_json TEXT NOT NULL, match_json TEXT NOT NULL, at REAL NOT NULL,\n PRIMARY KEY (recipient_key_json,generation_json,sender_json,match_json)\n) WITHOUT ROWID;\nCREATE TABLE square_posts (\n kind TEXT NOT NULL CHECK (kind IN ('activity','notice')), item_index INTEGER NOT NULL,\n recipient_key_json TEXT NOT NULL, token TEXT UNIQUE, owner_pid INTEGER,\n first_export_at REAL NOT NULL, claim_at REAL NOT NULL,\n posted INTEGER NOT NULL DEFAULT 0 CHECK (posted IN (0,1)), full INTEGER CHECK (full IN (0,1)),\n PRIMARY KEY (kind,item_index,recipient_key_json)\n) WITHOUT ROWID;";
|
|
5
|
+
export declare function initializeProducerSqlite(db: DatabaseSync): void;
|
|
6
|
+
export declare function resetProducerSqlite(db: DatabaseSync): void;
|
|
7
|
+
export declare function createProducerDriver(db: DatabaseSync): ProducerRead & ProducerWrite & {
|
|
8
|
+
readonly changed: boolean;
|
|
9
|
+
readonly deliveryChanged: boolean;
|
|
10
|
+
consumeActivity(index: number, at: number): void;
|
|
11
|
+
};
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { matchesMetadata, validateMetadataMatch } from './delivery-deferral.js';
|
|
2
|
+
import { SquareError, sameName, validateName, validateActivityMetadata } from './model.js';
|
|
3
|
+
import { decoded, encode, finite, integer, json, key, object } from './sqlite-values.js';
|
|
4
|
+
/** Private Square producer bookkeeping, on the artifact's existing connection/transaction. */
|
|
5
|
+
export const producerSchemaSql = `CREATE TABLE square_notices (
|
|
6
|
+
notice_index INTEGER PRIMARY KEY CHECK (notice_index >= 0), at REAL NOT NULL,
|
|
7
|
+
sender_json TEXT NOT NULL, body_json TEXT NOT NULL, metadata_json TEXT
|
|
8
|
+
);
|
|
9
|
+
CREATE TABLE notice_audience (
|
|
10
|
+
notice_index INTEGER NOT NULL, recipient_key_json TEXT NOT NULL, recipient_json TEXT NOT NULL,
|
|
11
|
+
owner_json TEXT NOT NULL, generation_json TEXT NOT NULL, consumed_at REAL,
|
|
12
|
+
PRIMARY KEY (notice_index,recipient_key_json)
|
|
13
|
+
) WITHOUT ROWID;
|
|
14
|
+
CREATE TABLE producer_consumption (
|
|
15
|
+
recipient_key_json TEXT NOT NULL, owner_json TEXT NOT NULL, generation_json TEXT NOT NULL,
|
|
16
|
+
sender_json TEXT NOT NULL, match_json TEXT NOT NULL, at REAL NOT NULL,
|
|
17
|
+
PRIMARY KEY (recipient_key_json,generation_json,sender_json,match_json)
|
|
18
|
+
) WITHOUT ROWID;
|
|
19
|
+
CREATE TABLE square_posts (
|
|
20
|
+
kind TEXT NOT NULL CHECK (kind IN ('activity','notice')), item_index INTEGER NOT NULL,
|
|
21
|
+
recipient_key_json TEXT NOT NULL, token TEXT UNIQUE, owner_pid INTEGER,
|
|
22
|
+
first_export_at REAL NOT NULL, claim_at REAL NOT NULL,
|
|
23
|
+
posted INTEGER NOT NULL DEFAULT 0 CHECK (posted IN (0,1)), full INTEGER CHECK (full IN (0,1)),
|
|
24
|
+
PRIMARY KEY (kind,item_index,recipient_key_json)
|
|
25
|
+
) WITHOUT ROWID;`;
|
|
26
|
+
export function initializeProducerSqlite(db) { db.exec(producerSchemaSql); }
|
|
27
|
+
export function resetProducerSqlite(db) {
|
|
28
|
+
for (const table of ['square_posts', 'producer_consumption', 'notice_audience', 'square_notices'])
|
|
29
|
+
db.exec(`DROP TABLE ${table}`);
|
|
30
|
+
}
|
|
31
|
+
function canonicalMatch(match) {
|
|
32
|
+
const fields = validateMetadataMatch(match).fields.map(field => ({ path: [...field.path], equals: field.equals }));
|
|
33
|
+
fields.sort((a, b) => {
|
|
34
|
+
const left = JSON.stringify(a.path), right = JSON.stringify(b.path);
|
|
35
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
36
|
+
});
|
|
37
|
+
return encode({ fields });
|
|
38
|
+
}
|
|
39
|
+
const EXPORT_IDENTITY_WINDOW_MS = 30 * 24 * 60 * 60 * 1000;
|
|
40
|
+
const EXPORT_OWNERSHIP_MS = 60_000;
|
|
41
|
+
function withinExportWindow(first, now) {
|
|
42
|
+
return now >= first && now - first < EXPORT_IDENTITY_WINDOW_MS;
|
|
43
|
+
}
|
|
44
|
+
function processIsAlive(pid) {
|
|
45
|
+
try {
|
|
46
|
+
process.kill(pid, 0);
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
return error.code !== 'ESRCH';
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
export function createProducerDriver(db) {
|
|
54
|
+
let changed = false;
|
|
55
|
+
const rows = (sql, args = []) => db.prepare(sql).all(...args);
|
|
56
|
+
const one = (sql, args = []) => db.prepare(sql).get(...args);
|
|
57
|
+
const run = (sql, args = []) => {
|
|
58
|
+
const count = Number(db.prepare(sql).run(...args).changes);
|
|
59
|
+
if (count)
|
|
60
|
+
changed = true;
|
|
61
|
+
return count;
|
|
62
|
+
};
|
|
63
|
+
function authority(seat) {
|
|
64
|
+
const current = one(`SELECT s.*,p.joined FROM participant_seats s JOIN participants p
|
|
65
|
+
ON p.name_key_json=s.participant_key_json WHERE s.participant_key_json=?`, [key(seat.participant)]);
|
|
66
|
+
if (!current || current.joined !== 1 || current.owner_json !== encode(seat.owner)
|
|
67
|
+
|| current.generation_json !== encode(seat.generation) || current.attachment !== 'attached')
|
|
68
|
+
throw new SquareError('already_joined', 'This participant seat is away or has been replaced.', { reason: 'needs_seat' });
|
|
69
|
+
}
|
|
70
|
+
function consumed(post) {
|
|
71
|
+
return rows(`SELECT sender_json,match_json FROM producer_consumption WHERE recipient_key_json=? AND owner_json=? AND generation_json=?`, [key(post.recipient), encode(post.owner), encode(post.generation)])
|
|
72
|
+
.some(row => sameName(decoded(row.sender_json, true), post.from) && matchesMetadata(post.metadata, json(row.match_json)));
|
|
73
|
+
}
|
|
74
|
+
function knownConsumed(post) {
|
|
75
|
+
return post.kind === 'activity'
|
|
76
|
+
? !!one('SELECT 1 FROM caught_activity WHERE recipient_key_json=? AND act_index=?', [key(post.recipient), post.index])
|
|
77
|
+
|| !!one(`SELECT 1 FROM activities WHERE kind='read' AND actor_key_json=? AND json_extract(payload_json,'$.through')>=?`, [key(post.recipient), post.index])
|
|
78
|
+
: one('SELECT consumed_at FROM notice_audience WHERE recipient_key_json=? AND notice_index=?', [key(post.recipient), post.index])?.consumed_at != null;
|
|
79
|
+
}
|
|
80
|
+
function consume(post, at) {
|
|
81
|
+
return post.kind === 'activity'
|
|
82
|
+
? run('INSERT INTO caught_activity(recipient_key_json,act_index,at) VALUES(?,?,?) ON CONFLICT DO NOTHING', [key(post.recipient), post.index, at])
|
|
83
|
+
: run('UPDATE notice_audience SET consumed_at=? WHERE recipient_key_json=? AND notice_index=? AND consumed_at IS NULL', [at, key(post.recipient), post.index]);
|
|
84
|
+
}
|
|
85
|
+
function allPosts() {
|
|
86
|
+
const activity = rows(`SELECT a.act_index,a.at,a.actor_json,a.payload_json,d.*,p.name_json,
|
|
87
|
+
x.full,x.posted FROM activities a JOIN landed_audience d ON d.act_index=a.act_index
|
|
88
|
+
JOIN participants p ON p.name_key_json=d.recipient_key_json
|
|
89
|
+
LEFT JOIN square_posts x ON x.kind='activity' AND x.item_index=a.act_index AND x.recipient_key_json=d.recipient_key_json
|
|
90
|
+
WHERE a.kind='say' AND d.owner_json IS NOT NULL AND d.generation_json IS NOT NULL ORDER BY a.act_index,d.recipient_order`)
|
|
91
|
+
.map((row) => {
|
|
92
|
+
const payload = object(json(row.payload_json));
|
|
93
|
+
return { kind: 'activity', index: integer(row.act_index), recipient: decoded(row.name_json, true),
|
|
94
|
+
owner: decoded(row.owner_json, true), generation: decoded(row.generation_json, true), from: decoded(row.actor_json, true), at: finite(row.at),
|
|
95
|
+
...(payload.body === undefined ? {} : { body: payload.body }),
|
|
96
|
+
...(payload.metadata === undefined ? {} : { metadata: payload.metadata }), route: row.reason,
|
|
97
|
+
...(row.full === null ? {} : { full: row.full === 1 }) };
|
|
98
|
+
});
|
|
99
|
+
const notices = rows(`SELECT n.*,a.*,x.full FROM square_notices n JOIN notice_audience a USING(notice_index)
|
|
100
|
+
LEFT JOIN square_posts x ON x.kind='notice' AND x.item_index=n.notice_index AND x.recipient_key_json=a.recipient_key_json ORDER BY n.notice_index,a.recipient_key_json`)
|
|
101
|
+
.map((row) => ({ kind: 'notice', index: integer(row.notice_index), recipient: decoded(row.recipient_json, true),
|
|
102
|
+
owner: decoded(row.owner_json, true), generation: decoded(row.generation_json, true), from: decoded(row.sender_json, true), at: finite(row.at),
|
|
103
|
+
body: decoded(row.body_json), ...(row.metadata_json === null ? {} : { metadata: json(row.metadata_json) }),
|
|
104
|
+
...(row.full === null ? {} : { full: row.full === 1 }) }));
|
|
105
|
+
return [...activity, ...notices].map(post => ({ ...post, consumed: knownConsumed(post) }));
|
|
106
|
+
}
|
|
107
|
+
const driver = {
|
|
108
|
+
pendingPosts() {
|
|
109
|
+
return allPosts().filter(post => !post.consumed && !one(`SELECT 1 FROM square_posts WHERE kind=? AND item_index=? AND recipient_key_json=? AND posted=1`, [post.kind, post.index, key(post.recipient)]));
|
|
110
|
+
},
|
|
111
|
+
postedPosts() {
|
|
112
|
+
return allPosts().filter(post => !!one(`SELECT 1 FROM square_posts WHERE kind=? AND item_index=? AND recipient_key_json=? AND (posted=1 OR token IS NOT NULL)`, [post.kind, post.index, key(post.recipient)]));
|
|
113
|
+
},
|
|
114
|
+
ownsPost(token, exportAt = Date.now()) {
|
|
115
|
+
const row = one('SELECT * FROM square_posts WHERE token=?', [token]);
|
|
116
|
+
return !!row && row.posted !== 1 && withinExportWindow(finite(row.first_export_at), finite(exportAt))
|
|
117
|
+
&& exportAt >= finite(row.claim_at) && exportAt - finite(row.claim_at) < EXPORT_OWNERSHIP_MS;
|
|
118
|
+
},
|
|
119
|
+
insertNotice(input, at) {
|
|
120
|
+
validateName(input.from);
|
|
121
|
+
if (typeof input.body !== 'string' || !input.body.trim())
|
|
122
|
+
throw new SquareError('invalid_args', 'A notification needs a non-empty body.');
|
|
123
|
+
const metadata = validateActivityMetadata(input.metadata);
|
|
124
|
+
const targets = typeof input.to === 'string' ? [input.to] : input.to;
|
|
125
|
+
if (!Array.isArray(targets) || targets.length < 1 || targets.length > 10)
|
|
126
|
+
throw new SquareError('invalid_args', 'A notification needs between 1 and 10 recipients.');
|
|
127
|
+
const recipients = [];
|
|
128
|
+
for (const name of targets) {
|
|
129
|
+
validateName(name);
|
|
130
|
+
if (recipients.some(row => sameName(decoded(row.participant_json, true), name)))
|
|
131
|
+
continue;
|
|
132
|
+
const row = one(`SELECT s.* FROM participant_seats s JOIN participants p ON p.name_key_json=s.participant_key_json WHERE p.joined=1 AND p.name_key_json=?`, [key(name)]);
|
|
133
|
+
if (!row)
|
|
134
|
+
throw new SquareError('invalid_args', `Notification recipient ${name} is not standing.`, { reason: 'not_standing', targets: [name] });
|
|
135
|
+
recipients.push(row);
|
|
136
|
+
}
|
|
137
|
+
const index = integer(one('SELECT COALESCE(MAX(notice_index),-1)+1 AS n FROM square_notices').n);
|
|
138
|
+
if (!Number.isSafeInteger(index + 1))
|
|
139
|
+
throw new SquareError('invalid_args', 'Notification index is exhausted.');
|
|
140
|
+
const body = input.body;
|
|
141
|
+
run('INSERT INTO square_notices VALUES(?,?,?,?,?)', [index, finite(at), encode(input.from), encode(body), metadata === undefined ? null : encode(metadata)]);
|
|
142
|
+
for (const row of recipients) {
|
|
143
|
+
const post = { kind: 'notice', index, recipient: decoded(row.participant_json, true), owner: decoded(row.owner_json, true),
|
|
144
|
+
generation: decoded(row.generation_json, true), from: input.from, body, at, ...(metadata === undefined ? {} : { metadata }) };
|
|
145
|
+
run('INSERT INTO notice_audience VALUES(?,?,?,?,?,?)', [index, key(post.recipient), encode(post.recipient), encode(post.owner), encode(post.generation), consumed(post) ? at : null]);
|
|
146
|
+
}
|
|
147
|
+
return { id: `notice/${index}`, at, from: input.from, to: recipients.map(row => decoded(row.participant_json, true)), body, ...(metadata === undefined ? {} : { metadata }) };
|
|
148
|
+
},
|
|
149
|
+
ackMatching(seat, input, at) {
|
|
150
|
+
authority(seat);
|
|
151
|
+
validateName(input.from);
|
|
152
|
+
if (input.future !== 'exact')
|
|
153
|
+
throw new SquareError('invalid_args', 'Future consumption requires an explicit exact outcome.');
|
|
154
|
+
const match = canonicalMatch(input.match);
|
|
155
|
+
run('INSERT INTO producer_consumption VALUES(?,?,?,?,?,?) ON CONFLICT DO NOTHING', [key(seat.participant), encode(seat.owner), encode(seat.generation), key(input.from), match, finite(at)]);
|
|
156
|
+
let count = 0;
|
|
157
|
+
for (const post of allPosts()) {
|
|
158
|
+
if (sameName(post.recipient, seat.participant) && post.owner === seat.owner && post.generation === seat.generation
|
|
159
|
+
&& sameName(post.from, input.from) && matchesMetadata(post.metadata, input.match) && !post.consumed)
|
|
160
|
+
count += consume(post, at);
|
|
161
|
+
}
|
|
162
|
+
return count;
|
|
163
|
+
},
|
|
164
|
+
acquirePost(post, token, pid, now, exportAt = Date.now()) {
|
|
165
|
+
// A scanned value is not custody. Re-read all source and policy facts under the World writer.
|
|
166
|
+
const current = allPosts().find(candidate => candidate.kind === post.kind && candidate.index === post.index && candidate.recipient === post.recipient);
|
|
167
|
+
if (!current || current.owner !== post.owner || current.generation !== post.generation || current.consumed)
|
|
168
|
+
return false;
|
|
169
|
+
const seat = one('SELECT * FROM participant_seats WHERE participant_key_json=?', [key(post.recipient)]);
|
|
170
|
+
if (!seat || seat.owner_json !== encode(post.owner) || seat.generation_json !== encode(post.generation) || seat.attachment !== 'attached')
|
|
171
|
+
return false;
|
|
172
|
+
if (consumed(current)) {
|
|
173
|
+
consume(current, now);
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
const previous = one('SELECT * FROM square_posts WHERE kind=? AND item_index=? AND recipient_key_json=?', [post.kind, post.index, key(post.recipient)]);
|
|
177
|
+
finite(exportAt);
|
|
178
|
+
if (previous?.posted === 1)
|
|
179
|
+
return false;
|
|
180
|
+
const first = previous ? finite(previous.first_export_at) : exportAt;
|
|
181
|
+
// Missing ledger identity after retention cannot prove an abandoned export was unsent.
|
|
182
|
+
if (!withinExportWindow(first, exportAt) || previous && exportAt < finite(previous.claim_at))
|
|
183
|
+
return false;
|
|
184
|
+
if (previous?.token != null && exportAt >= finite(previous.claim_at)
|
|
185
|
+
&& exportAt - finite(previous.claim_at) < EXPORT_OWNERSHIP_MS && processIsAlive(integer(previous.owner_pid, 1)))
|
|
186
|
+
return false;
|
|
187
|
+
run(`INSERT INTO square_posts(kind,item_index,recipient_key_json,token,owner_pid,full,first_export_at,claim_at) VALUES(?,?,?,?,?,?,?,?)
|
|
188
|
+
ON CONFLICT(kind,item_index,recipient_key_json) DO UPDATE SET token=excluded.token,owner_pid=excluded.owner_pid,full=excluded.full,claim_at=excluded.claim_at`, [post.kind, post.index, key(post.recipient), token, integer(pid, 1), post.full === undefined ? null : Number(post.full), first, exportAt]);
|
|
189
|
+
return previous ? 'recovered' : 'initial';
|
|
190
|
+
},
|
|
191
|
+
settlePost(token, full, disposition) {
|
|
192
|
+
// Only a proved-unsent initial claim may erase its export age; prior uncertainty survives.
|
|
193
|
+
if (disposition === 'never-exported')
|
|
194
|
+
return run('DELETE FROM square_posts WHERE token=?', [token]) > 0;
|
|
195
|
+
return run('UPDATE square_posts SET token=NULL,owner_pid=NULL,posted=?,full=? WHERE token=?', [Number(disposition === 'posted'), Number(full), token]) > 0;
|
|
196
|
+
},
|
|
197
|
+
foldReceipt(post, at) {
|
|
198
|
+
const current = allPosts().find(candidate => candidate.kind === post.kind && candidate.index === post.index && candidate.recipient === post.recipient);
|
|
199
|
+
if (!current || current.owner !== post.owner || current.generation !== post.generation || current.full !== true || current.consumed)
|
|
200
|
+
return false;
|
|
201
|
+
return consume(current, finite(at)) > 0;
|
|
202
|
+
},
|
|
203
|
+
consumeActivity(index, at) {
|
|
204
|
+
for (const post of allPosts())
|
|
205
|
+
if (post.kind === 'activity' && post.index === index && consumed(post))
|
|
206
|
+
consume(post, at);
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
return Object.defineProperties(driver, { changed: { get: () => changed }, deliveryChanged: { get: () => changed } });
|
|
210
|
+
}
|