@astrosheep/square 0.3.105 → 0.4.0
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 +8 -12
- package/dist/artifact.js +66 -343
- package/dist/attention-presentation.d.ts +33 -7
- package/dist/attention-presentation.js +33 -69
- package/dist/boundary-presentation.d.ts +0 -2
- package/dist/boundary-presentation.js +17 -22
- package/dist/catch-decisions.d.ts +2 -2
- package/dist/catch-decisions.js +14 -4
- package/dist/catch-selection.d.ts +8 -0
- package/dist/catch-selection.js +5 -0
- package/dist/channel-artifact.d.ts +10 -0
- package/dist/channel-artifact.js +55 -0
- package/dist/channel-delivery.d.ts +11 -0
- package/dist/channel-delivery.js +51 -0
- package/dist/channel-domain.d.ts +3 -0
- package/dist/channel-domain.js +83 -0
- package/dist/channel-facade.d.ts +76 -0
- package/dist/channel-facade.js +1 -0
- package/dist/channel-internal.d.ts +126 -0
- package/dist/channel-internal.js +440 -0
- package/dist/channel-metadata.d.ts +11 -0
- package/dist/channel-metadata.js +1 -0
- package/dist/channel-ports.d.ts +241 -0
- package/dist/channel-ports.js +1 -0
- package/dist/channel-presentation.d.ts +17 -0
- package/dist/channel-presentation.js +22 -0
- package/dist/channel-sqlite-schema.d.ts +13 -0
- package/dist/channel-sqlite-schema.js +81 -0
- package/dist/channel-transport.d.ts +62 -0
- package/dist/channel-transport.js +1 -0
- package/dist/channel-values.d.ts +27 -0
- package/dist/channel-values.js +90 -0
- package/dist/channel.d.ts +31 -21
- package/dist/channel.js +42 -74
- package/dist/claude-delivery-status.js +1 -1
- package/dist/claude-delivery.d.ts +2 -5
- package/dist/claude-delivery.js +56 -103
- package/dist/claude-mod.js +4 -4
- package/dist/claude-transport.d.ts +5 -0
- package/dist/claude-transport.js +58 -0
- package/dist/cli/context.d.ts +2 -2
- package/dist/cli/context.js +0 -1
- package/dist/cli/maintenance-commands.d.ts +0 -4
- package/dist/cli/maintenance-commands.js +2 -29
- package/dist/cli/program.js +1 -1
- package/dist/cli/registry.js +1 -2
- package/dist/codex-address.d.ts +1 -1
- package/dist/codex-queue.d.ts +2 -2
- package/dist/codex-queue.js +1 -3
- package/dist/decisions.js +3 -3
- package/dist/delivery-deferral.d.ts +9 -18
- package/dist/delivery-deferral.js +4 -4
- package/dist/delivery-health.d.ts +4 -4
- package/dist/delivery-health.js +11 -11
- package/dist/delivery-operations.d.ts +15 -2
- package/dist/delivery-operations.js +60 -30
- package/dist/delivery.d.ts +44 -33
- package/dist/delivery.js +49 -75
- package/dist/help.js +1 -6
- package/dist/http-delivery.d.ts +2 -2
- package/dist/http-delivery.js +2 -4
- package/dist/inbox.d.ts +10 -18
- package/dist/inbox.js +44 -53
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- 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 +41 -77
- package/dist/model.js +7 -139
- package/dist/neutral.d.ts +72 -0
- package/dist/neutral.js +27 -0
- package/dist/notifications.d.ts +2 -8
- package/dist/notifications.js +15 -50
- 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/paseo-delivery.d.ts +1 -1
- package/dist/paseo-delivery.js +2 -4
- package/dist/paseo-timeline.js +1 -0
- package/dist/plugins/opencode/delivery.js +27 -42
- package/dist/ports.d.ts +46 -168
- package/dist/presentation-operations.d.ts +1 -14
- package/dist/presentation-operations.js +16 -16
- 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/sqlite-channel.d.ts +15 -0
- package/dist/sqlite-channel.js +600 -0
- package/dist/sqlite-framing.d.ts +8 -0
- package/dist/sqlite-framing.js +31 -0
- package/dist/sqlite-square.d.ts +12 -13
- package/dist/sqlite-square.js +192 -248
- 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 +1 -1
- package/dist/square-core.d.ts +1 -1
- package/dist/square-domain.d.ts +21 -7
- package/dist/square-domain.js +93 -94
- package/dist/square-facade.d.ts +12 -66
- package/dist/square-index.d.ts +1 -1
- package/dist/square-index.js +1 -1
- package/dist/square-location.d.ts +6 -1
- package/dist/square-location.js +1 -1
- package/dist/square-receive.d.ts +2 -3
- package/dist/square-sqlite.d.ts +45 -44
- package/dist/square-sqlite.js +308 -764
- package/dist/square-tools.d.ts +8 -0
- package/dist/square-tools.js +39 -0
- package/dist/views.d.ts +6 -6
- package/dist/views.js +15 -9
- package/dist/wake-preview.d.ts +7 -0
- package/dist/wake-preview.js +15 -0
- package/dist/wake-sink.d.ts +2 -2
- package/dist/wake-sink.js +1 -1
- package/dist/wake-transports.d.ts +7 -0
- package/dist/wake-transports.js +42 -0
- package/guides/claude-native.md +13 -10
- package/guides/codex-delivery.md +3 -1
- package/guides/participant-ownership.md +1 -1
- package/guides/row-runtime-delivery-evidence.md +2 -0
- package/guides/row-storage-design.md +19 -44
- package/guides/square-api.md +137 -60
- package/package.json +3 -3
- package/plugins/claude/.claude-plugin/plugin.json +1 -1
- package/plugins/claude/types.d.ts +1 -1
- package/plugins/codex/.codex-plugin/plugin.json +1 -1
- package/plugins/opencode/delivery.ts +28 -41
- package/plugins/pi/square-pi.js +28 -51
- 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
|
@@ -0,0 +1,600 @@
|
|
|
1
|
+
import { validateOwner, nameKey } from './neutral.js';
|
|
2
|
+
import { isWakeRouteKind, validateMessageMetadata } from './channel-values.js';
|
|
3
|
+
import { validateMetadataMatch, matchingDeferrals, validateDeferralId } from './delivery-deferral.js';
|
|
4
|
+
import { claimAttempt, prepareAttempt, settleAttempt } from './delivery-attempts.js';
|
|
5
|
+
import { receive, recordConsumed } from './receive.js';
|
|
6
|
+
import { formatMessageId, parseMessageId, validateEndpointGeneration } from './message.js';
|
|
7
|
+
import { compileSearchPattern } from './search.js';
|
|
8
|
+
import { readStorageRevision } from './sqlite-framing.js';
|
|
9
|
+
import { encode, key, invalidArtifact, requireFact, integer, finite, string, json, object, shape, decoded, immutable, stableJson, pageRows, pageLimit } from './sqlite-values.js';
|
|
10
|
+
function message(row) {
|
|
11
|
+
const index = integer(row.msg_index), at = finite(row.at);
|
|
12
|
+
const from = decoded(row.from_json, true);
|
|
13
|
+
requireFact(row.from_key_json === key(from));
|
|
14
|
+
const metadata = row.metadata_json === null ? undefined : validateMessageMetadata(json(row.metadata_json));
|
|
15
|
+
return immutable({ index, at, from, body: decoded(row.body_json),
|
|
16
|
+
...(metadata === undefined ? {} : { metadata }),
|
|
17
|
+
...(row.preview === null || row.preview === undefined ? {} : { preview: string(row.preview, true) }) });
|
|
18
|
+
}
|
|
19
|
+
export function messageAddress(row) {
|
|
20
|
+
const recipient = decoded(row.recipient_json, true);
|
|
21
|
+
requireFact(row.recipient_key_json === key(recipient), 'message address identity is malformed.');
|
|
22
|
+
integer(row.msg_index);
|
|
23
|
+
integer(row.recipient_order);
|
|
24
|
+
const generation = row.generation_json === null ? undefined : endpointGeneration(decoded(row.generation_json, true));
|
|
25
|
+
return immutable({ recipient, ...(generation === undefined ? {} : { generation }) });
|
|
26
|
+
}
|
|
27
|
+
export function endpointGeneration(value) {
|
|
28
|
+
try {
|
|
29
|
+
return validateEndpointGeneration(value);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
throw invalidArtifact('endpoint generation is malformed.');
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function messageId(value) {
|
|
36
|
+
requireFact(typeof value === 'string' && parseMessageId(value) !== undefined, 'message id is malformed.');
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
function wakeAddress(value) {
|
|
40
|
+
const address = shape(value, ['kind', 'address']);
|
|
41
|
+
requireFact(isWakeRouteKind(address.kind));
|
|
42
|
+
const entries = object(address.address);
|
|
43
|
+
requireFact(Object.values(entries).every((item) => typeof item === 'string'));
|
|
44
|
+
stableJson(entries);
|
|
45
|
+
return immutable({ kind: address.kind, address: entries });
|
|
46
|
+
}
|
|
47
|
+
function ownerFrom(row, column = 'owner_json') {
|
|
48
|
+
const owner = decoded(row[column], true);
|
|
49
|
+
validateOwner(owner);
|
|
50
|
+
return owner;
|
|
51
|
+
}
|
|
52
|
+
export function endpoint(row) {
|
|
53
|
+
const participant = decoded(row.participant_json, true);
|
|
54
|
+
requireFact(row.participant_key_json === key(participant));
|
|
55
|
+
const generation = endpointGeneration(decoded(row.generation_json, true));
|
|
56
|
+
const parsedWake = row.wake_address_json === null ? undefined : wakeAddress(json(row.wake_address_json));
|
|
57
|
+
requireFact(row.attachment === 'attached' || row.attachment === 'detached', 'endpoint attachment is malformed.');
|
|
58
|
+
requireFact(row.attachment !== 'detached' || parsedWake === undefined, 'a detached endpoint has a wake address.');
|
|
59
|
+
return immutable({ participant, owner: ownerFrom(row), generation, attachment: row.attachment, ...(parsedWake === undefined ? {} : { wakeAddress: parsedWake }) });
|
|
60
|
+
}
|
|
61
|
+
function nativeCorrelation(value) {
|
|
62
|
+
const item = shape(value, ['harness'], ['token', 'messageId', 'payloadHash']);
|
|
63
|
+
requireFact(['claude', 'opencode', 'pi'].includes(string(item.harness)));
|
|
64
|
+
for (const key of ['token', 'messageId', 'payloadHash'])
|
|
65
|
+
if (Object.hasOwn(item, key))
|
|
66
|
+
string(item[key], true);
|
|
67
|
+
return immutable({ harness: item.harness,
|
|
68
|
+
...(item.token === undefined ? {} : { token: item.token }),
|
|
69
|
+
...(item.messageId === undefined ? {} : { messageId: item.messageId }),
|
|
70
|
+
...(item.payloadHash === undefined ? {} : { payloadHash: item.payloadHash }) });
|
|
71
|
+
}
|
|
72
|
+
export function deliveryEvidence(row, payload = object(json(row.payload_json))) {
|
|
73
|
+
const participant = decoded(row.participant_json, true);
|
|
74
|
+
requireFact(row.participant_key_json === key(participant));
|
|
75
|
+
const generation = endpointGeneration(decoded(row.generation_json, true));
|
|
76
|
+
const id = messageId(formatMessageId(integer(row.msg_index)));
|
|
77
|
+
requireFact(['wake', 'presentation'].includes(string(row.kind)) && ['dispatching', 'accepted', 'failed', 'unknown', 'released', 'presented', 'clipped'].includes(string(row.outcome)));
|
|
78
|
+
requireFact(Number.isSafeInteger(row.attempt_n) && row.attempt_n >= 0);
|
|
79
|
+
const nativeObservations = payload.nativeObservations === undefined ? undefined : (() => {
|
|
80
|
+
requireFact(Array.isArray(payload.nativeObservations));
|
|
81
|
+
return payload.nativeObservations.map((value) => {
|
|
82
|
+
const item = shape(value, ['eventId', 'messageId', 'state', 'observedAt'], ['reason']);
|
|
83
|
+
requireFact(['held', 'released', 'dropped'].includes(string(item.state)));
|
|
84
|
+
return {
|
|
85
|
+
eventId: string(item.eventId, true), messageId: string(item.messageId, true),
|
|
86
|
+
state: item.state,
|
|
87
|
+
...(Object.hasOwn(item, 'reason') ? { reason: string(item.reason) } : {}), observedAt: finite(item.observedAt),
|
|
88
|
+
};
|
|
89
|
+
});
|
|
90
|
+
})();
|
|
91
|
+
const coverage = payload.coverage === undefined ? undefined : (() => { requireFact(payload.coverage === 'full' || payload.coverage === 'clipped'); return payload.coverage; })();
|
|
92
|
+
return immutable({
|
|
93
|
+
participant, generation, message: id, kind: row.kind,
|
|
94
|
+
outcome: row.outcome, attemptN: integer(row.attempt_n), at: finite(row.at),
|
|
95
|
+
...(row.expires_at === null ? {} : { expiresAt: finite(row.expires_at) }),
|
|
96
|
+
...(row.claim_token_json === null ? {} : { claimToken: decoded(row.claim_token_json, true) }),
|
|
97
|
+
...(payload.route === undefined ? {} : { route: wakeAddress(payload.route) }),
|
|
98
|
+
...(payload.signature === undefined ? {} : { signature: string(payload.signature) }),
|
|
99
|
+
...(payload.message === undefined ? {} : { messageText: string(payload.message) }),
|
|
100
|
+
...(payload.diagnostic === undefined ? {} : { diagnostic: payload.diagnostic }),
|
|
101
|
+
...(payload.correlation === undefined ? {} : { correlation: nativeCorrelation(payload.correlation) }),
|
|
102
|
+
...(coverage === undefined ? {} : { coverage }),
|
|
103
|
+
...(payload.description === undefined ? {} : { description: string(payload.description) }),
|
|
104
|
+
...(nativeObservations === undefined ? {} : { nativeObservations }),
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
export function deliveryDeferral(row) {
|
|
108
|
+
const participant = decoded(row.participant_json, true);
|
|
109
|
+
requireFact(row.participant_key_json === key(participant));
|
|
110
|
+
const generation = endpointGeneration(decoded(row.generation_json, true));
|
|
111
|
+
const id = validateDeferralId(decoded(row.deferral_id_json, true));
|
|
112
|
+
const match = validateMetadataMatch(json(row.match_json));
|
|
113
|
+
requireFact(encode(match) === row.match_json, 'delivery deferral match encoding is malformed.');
|
|
114
|
+
return immutable({ participant, generation, id, match, expiresAt: finite(row.expires_at) });
|
|
115
|
+
}
|
|
116
|
+
export function receiveClaim(row) {
|
|
117
|
+
const owner = decoded(row.owner_key_json, true), namespace = decoded(row.namespace_key_json, true);
|
|
118
|
+
const generation = endpointGeneration(decoded(row.generation_json, true));
|
|
119
|
+
const suppressed = json(row.suppressed_json);
|
|
120
|
+
requireFact(Array.isArray(suppressed) && suppressed.every(value => Number.isSafeInteger(value) && value >= 0), 'receive claim coverage is malformed.');
|
|
121
|
+
return immutable({ namespace, id: decoded(row.claim_id_json, true), owner, generation,
|
|
122
|
+
selector: json(row.selector_json), suppressed: suppressed,
|
|
123
|
+
heartbeatAt: finite(row.heartbeat_at), expiresAt: finite(row.expires_at) });
|
|
124
|
+
}
|
|
125
|
+
function messageSelection(filter, alias = 'm') {
|
|
126
|
+
const terms = [], args = [];
|
|
127
|
+
const add = (sql, value) => {
|
|
128
|
+
terms.push(sql);
|
|
129
|
+
args.push(value);
|
|
130
|
+
};
|
|
131
|
+
if (filter.afterIndex !== undefined)
|
|
132
|
+
add(`${alias}.msg_index > ?`, integer(filter.afterIndex, -1));
|
|
133
|
+
if (filter.beforeIndex !== undefined)
|
|
134
|
+
add(`${alias}.msg_index < ?`, integer(filter.beforeIndex));
|
|
135
|
+
if (filter.throughIndex !== undefined)
|
|
136
|
+
add(`${alias}.msg_index <= ?`, integer(filter.throughIndex, -1));
|
|
137
|
+
if (filter.afterAt !== undefined)
|
|
138
|
+
add(`${alias}.at > ?`, finite(filter.afterAt));
|
|
139
|
+
for (const [column, values] of [['msg_index', filter.indexes], [filter.senderMatch === 'exact' ? 'from_json' : 'from_key_json', filter.senders?.map(name => filter.senderMatch === 'exact' ? encode(name) : key(name))]]) {
|
|
140
|
+
if (values !== undefined) {
|
|
141
|
+
if (column === 'msg_index')
|
|
142
|
+
values.forEach(v => integer(v));
|
|
143
|
+
add(`${alias}.${column} IN (SELECT value FROM json_each(?))`, encode(values));
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (filter.metadataMatch !== undefined) {
|
|
147
|
+
terms.push(`NOT EXISTS (
|
|
148
|
+
SELECT 1 FROM json_each(?) field WHERE NOT EXISTS (
|
|
149
|
+
WITH RECURSIVE walk(value,type,depth) AS (
|
|
150
|
+
SELECT json_extract(${alias}.metadata_json,'$'),json_type(${alias}.metadata_json,'$'),0
|
|
151
|
+
UNION ALL
|
|
152
|
+
SELECT child.value,child.type,walk.depth+1
|
|
153
|
+
FROM walk JOIN json_each(CASE WHEN walk.type='object' THEN walk.value ELSE '{}' END) child
|
|
154
|
+
ON child.key=json_extract(field.value,'$.path['||walk.depth||']')
|
|
155
|
+
WHERE walk.depth<json_array_length(field.value,'$.path')
|
|
156
|
+
)
|
|
157
|
+
SELECT 1 FROM walk WHERE depth=json_array_length(field.value,'$.path')
|
|
158
|
+
AND (type=json_type(field.value,'$.equals')
|
|
159
|
+
OR type IN ('integer','real') AND json_type(field.value,'$.equals') IN ('integer','real'))
|
|
160
|
+
AND value IS json_extract(field.value,'$.equals')
|
|
161
|
+
)
|
|
162
|
+
)`);
|
|
163
|
+
args.push(encode(filter.metadataMatch.fields));
|
|
164
|
+
}
|
|
165
|
+
if (filter.search !== undefined) {
|
|
166
|
+
terms.push(`square_search(?,?,'msg/'||${alias}.msg_index,${alias}.from_json,json_extract(${alias}.body_json,'$'))=1`);
|
|
167
|
+
args.push(filter.search.mode, filter.search.text);
|
|
168
|
+
}
|
|
169
|
+
return { sql: terms.length ? terms.join(' AND ') : '1', args };
|
|
170
|
+
}
|
|
171
|
+
export function createChannelSqlite(db) {
|
|
172
|
+
let changed = false, deliveryChanged = false;
|
|
173
|
+
const all = (sql, args = []) => db.prepare(sql).all(...args);
|
|
174
|
+
const one = (sql, args = []) => db.prepare(sql).get(...args);
|
|
175
|
+
const run = (sql, args = []) => Number(db.prepare(sql).run(...args).changes);
|
|
176
|
+
let searchRegistered = false;
|
|
177
|
+
function ensureSearch() {
|
|
178
|
+
if (searchRegistered)
|
|
179
|
+
return;
|
|
180
|
+
const patterns = new Map();
|
|
181
|
+
db.function('square_search', { deterministic: true }, (mode, text, index, actor, body) => {
|
|
182
|
+
const cacheKey = encode([mode, text]);
|
|
183
|
+
let pattern = patterns.get(cacheKey);
|
|
184
|
+
if (pattern === undefined) {
|
|
185
|
+
pattern = compileSearchPattern(string(text), mode === 'literal');
|
|
186
|
+
patterns.set(cacheKey, pattern);
|
|
187
|
+
}
|
|
188
|
+
return Number([string(index), decoded(actor, true), body === null ? '' : string(body)]
|
|
189
|
+
.some(field => pattern.test(field)));
|
|
190
|
+
});
|
|
191
|
+
searchRegistered = true;
|
|
192
|
+
}
|
|
193
|
+
function selectMessage(filter) {
|
|
194
|
+
if (filter.search !== undefined)
|
|
195
|
+
ensureSearch();
|
|
196
|
+
return messageSelection(filter);
|
|
197
|
+
}
|
|
198
|
+
const channel = {};
|
|
199
|
+
function currentEndpoint(name) {
|
|
200
|
+
const r = one('SELECT * FROM endpoints WHERE participant_key_json=?', [key(name)]);
|
|
201
|
+
return r ? endpoint(r) : undefined;
|
|
202
|
+
}
|
|
203
|
+
function owner(requested) {
|
|
204
|
+
const current = currentEndpoint(requested);
|
|
205
|
+
if (current !== undefined)
|
|
206
|
+
return current.participant;
|
|
207
|
+
const historical = one('SELECT recipient_json FROM message_addresses WHERE recipient_key_json=? ORDER BY msg_index LIMIT 1', [key(requested)]);
|
|
208
|
+
return historical === undefined ? requested : decoded(historical.recipient_json, true);
|
|
209
|
+
}
|
|
210
|
+
function directedSelection(recipient, filter) {
|
|
211
|
+
const endpoint = currentEndpoint(recipient), canonical = endpoint?.participant ?? recipient, base = selectMessage(filter);
|
|
212
|
+
base.sql += ' AND ma.recipient_key_json=?';
|
|
213
|
+
base.args.push(key(canonical));
|
|
214
|
+
if (filter.currentGeneration) {
|
|
215
|
+
base.sql += ' AND (ma.generation_json IS NULL OR ma.generation_json=?)';
|
|
216
|
+
base.args.push(endpoint === undefined ? encode('') : encode(endpoint.generation));
|
|
217
|
+
}
|
|
218
|
+
if (filter.unseenOnly) {
|
|
219
|
+
base.sql += ' AND NOT EXISTS (SELECT 1 FROM receipts r WHERE r.owner_key_json=? AND r.msg_index=m.msg_index)';
|
|
220
|
+
base.args.push(encode(canonical));
|
|
221
|
+
}
|
|
222
|
+
return base;
|
|
223
|
+
}
|
|
224
|
+
// ----- Lower Channel capabilities -----
|
|
225
|
+
const channelRead = {
|
|
226
|
+
header() {
|
|
227
|
+
const rows = all('SELECT m.*, (SELECT COALESCE(MAX(msg_index),-1) FROM messages) AS last_msg_index FROM channel_meta m');
|
|
228
|
+
requireFact(rows.length === 1 && rows[0].id === 1, 'header row is malformed.');
|
|
229
|
+
const r = rows[0], nextMessageIndex = integer(r.next_msg_index), lastMessageIndex = integer(r.last_msg_index, -1);
|
|
230
|
+
requireFact(nextMessageIndex > lastMessageIndex, 'nextMessageIndex is behind the message history.');
|
|
231
|
+
return immutable({ revision: readStorageRevision(db), deliveryRevision: readStorageRevision(db, true), nextMessageIndex, lastMessageIndex });
|
|
232
|
+
},
|
|
233
|
+
message(index) { const r = one('SELECT * FROM messages WHERE msg_index=?', [integer(index)]); return r ? message(r) : undefined; },
|
|
234
|
+
messages(indexes) {
|
|
235
|
+
indexes.forEach(v => integer(v));
|
|
236
|
+
return immutable(all('SELECT * FROM messages WHERE msg_index IN (SELECT value FROM json_each(?)) ORDER BY msg_index', [encode(indexes)]).map(message));
|
|
237
|
+
},
|
|
238
|
+
messageHeads(filter, page) {
|
|
239
|
+
const s = selectMessage(filter), limit = pageLimit(page);
|
|
240
|
+
return pageRows(all(`SELECT m.* FROM messages m WHERE ${s.sql} ORDER BY m.msg_index ${page.edge === 'newest' ? 'DESC' : 'ASC'} LIMIT ?`, [...s.args, limit]), page, message);
|
|
241
|
+
},
|
|
242
|
+
messageCount(filter) { const s = selectMessage(filter); return integer(one(`SELECT COUNT(*) AS n FROM messages m WHERE ${s.sql}`, s.args).n); },
|
|
243
|
+
addresses(index) {
|
|
244
|
+
return immutable(all('SELECT * FROM message_addresses WHERE msg_index=? ORDER BY recipient_order', [integer(index)]).map(messageAddress));
|
|
245
|
+
},
|
|
246
|
+
receipt(name, index) { const r = one('SELECT at FROM receipts WHERE owner_key_json=? AND msg_index=?', [encode(owner(name)), integer(index)]); return r ? immutable({ at: finite(r.at) }) : undefined; },
|
|
247
|
+
receiptFacts(name, afterIndex) {
|
|
248
|
+
const k = encode(owner(name));
|
|
249
|
+
const r = one('SELECT MAX(at) AS latest FROM receipts WHERE owner_key_json=?', [k]);
|
|
250
|
+
const e = one(`SELECT MAX(r.msg_index) AS latest FROM receipts r JOIN messages m ON m.msg_index=r.msg_index WHERE r.owner_key_json=? AND r.msg_index>?`, [k, integer(afterIndex, -1)]);
|
|
251
|
+
return immutable({ ...(r.latest === null ? {} : { latestAt: finite(r.latest) }), ...(e.latest === null ? {} : { latestActorMessageIndex: integer(e.latest) }) });
|
|
252
|
+
},
|
|
253
|
+
directed(recipient, filter, page) {
|
|
254
|
+
const base = directedSelection(recipient, filter), limit = pageLimit(page);
|
|
255
|
+
const rows = all(`SELECT m.*,ma.recipient_json,ma.generation_json FROM messages m JOIN message_addresses ma ON ma.msg_index=m.msg_index WHERE ${base.sql} ORDER BY m.msg_index ${page.edge === 'newest' ? 'DESC' : 'ASC'} LIMIT ?`, [...base.args, limit]);
|
|
256
|
+
return pageRows(rows, page, r => ({ index: integer(r.msg_index), at: finite(r.at), from: decoded(r.from_json, true),
|
|
257
|
+
recipient: decoded(r.recipient_json, true), ...(r.generation_json === null ? {} : { generation: endpointGeneration(decoded(r.generation_json, true)) }) }));
|
|
258
|
+
},
|
|
259
|
+
directedCount(recipient, filter) {
|
|
260
|
+
const base = directedSelection(recipient, filter);
|
|
261
|
+
return integer(one(`SELECT COUNT(*) AS n FROM messages m JOIN message_addresses ma ON ma.msg_index=m.msg_index WHERE ${base.sql}`, base.args).n);
|
|
262
|
+
},
|
|
263
|
+
endpoint(name) { return currentEndpoint(name); },
|
|
264
|
+
endpointsForOwner(requested) {
|
|
265
|
+
validateOwner(requested);
|
|
266
|
+
return immutable(all('SELECT * FROM endpoints WHERE owner_json=? ORDER BY participant_key_json', [encode(requested)]).map(endpoint));
|
|
267
|
+
},
|
|
268
|
+
endpoints() { return immutable(all('SELECT * FROM endpoints ORDER BY participant_key_json').map(endpoint)); },
|
|
269
|
+
deliveryEvidence(participant, generation, activity) {
|
|
270
|
+
const args = [key(participant), encode(endpointGeneration(generation))];
|
|
271
|
+
const filter = activity === undefined ? '' : ' AND msg_index=?';
|
|
272
|
+
if (activity !== undefined) {
|
|
273
|
+
const resolved = parseMessageId(activity);
|
|
274
|
+
requireFact(resolved !== undefined, 'message id is malformed.');
|
|
275
|
+
args.push(resolved);
|
|
276
|
+
}
|
|
277
|
+
return immutable(all(`SELECT * FROM delivery_evidence WHERE participant_key_json=? AND generation_json=?${filter} ORDER BY msg_index,kind,attempt_n`, args).map(row => deliveryEvidence(row)));
|
|
278
|
+
},
|
|
279
|
+
deliveryByToken(claimToken) { const row = one('SELECT * FROM delivery_evidence WHERE claim_token_json=?', [encode(claimToken)]); return row === undefined ? undefined : deliveryEvidence(row); },
|
|
280
|
+
cancelledThrough(participant, generation) {
|
|
281
|
+
const r = one('SELECT through_index FROM delivery_cancellations WHERE participant_key_json=? AND generation_json=?', [key(participant), encode(endpointGeneration(generation))]);
|
|
282
|
+
return r === undefined ? undefined : integer(r.through_index, -1);
|
|
283
|
+
},
|
|
284
|
+
deliveryDeferrals(name, generation) {
|
|
285
|
+
const canonical = owner(name), current = currentEndpoint(canonical);
|
|
286
|
+
if (current === undefined || current.generation !== generation)
|
|
287
|
+
return [];
|
|
288
|
+
return immutable(all('SELECT * FROM delivery_deferrals WHERE participant_key_json=? AND generation_json=? ORDER BY expires_at,deferral_id_json', [key(canonical), encode(generation)]).map(deliveryDeferral));
|
|
289
|
+
},
|
|
290
|
+
receiveClaims(name, namespace) {
|
|
291
|
+
const canonical = owner(name), args = [encode(canonical)];
|
|
292
|
+
let where = 'owner_key_json=?';
|
|
293
|
+
if (namespace !== undefined) {
|
|
294
|
+
where += ' AND namespace_key_json=?';
|
|
295
|
+
args.push(encode(namespace));
|
|
296
|
+
}
|
|
297
|
+
return immutable(all(`SELECT * FROM receive_claims WHERE ${where} ORDER BY namespace_key_json`, args).map(receiveClaim));
|
|
298
|
+
},
|
|
299
|
+
claimLease(name, namespace) { return channelRead.receiveClaims(name, namespace)[0]; },
|
|
300
|
+
suppressed(name, generation, index, now) {
|
|
301
|
+
integer(index);
|
|
302
|
+
finite(now);
|
|
303
|
+
endpointGeneration(generation);
|
|
304
|
+
const rows = all('SELECT * FROM receive_claims WHERE owner_key_json=? AND expires_at>?', [encode(owner(name)), now]);
|
|
305
|
+
return rows.some(row => {
|
|
306
|
+
const claim = receiveClaim(row);
|
|
307
|
+
return claim.generation === generation && claim.suppressed.includes(index);
|
|
308
|
+
});
|
|
309
|
+
},
|
|
310
|
+
};
|
|
311
|
+
function writeMessage(draft) {
|
|
312
|
+
const from = string(draft.from, true), body = string(draft.body);
|
|
313
|
+
requireFact(Array.isArray(draft.to), 'message addresses are malformed.');
|
|
314
|
+
const next = integer(one('SELECT next_msg_index FROM channel_meta WHERE id=1').next_msg_index);
|
|
315
|
+
requireFact(next < Number.MAX_SAFE_INTEGER, 'message index is exhausted.');
|
|
316
|
+
const metadata = draft.metadata === undefined ? undefined : validateMessageMetadata(draft.metadata);
|
|
317
|
+
if (draft.preview !== undefined)
|
|
318
|
+
string(draft.preview, true);
|
|
319
|
+
const seen = new Set();
|
|
320
|
+
const audience = draft.to.flatMap(address => {
|
|
321
|
+
const recipient = string(address.recipient, true);
|
|
322
|
+
if (seen.has(nameKey(recipient)))
|
|
323
|
+
return [];
|
|
324
|
+
seen.add(nameKey(recipient));
|
|
325
|
+
return [{ recipient, via: address }];
|
|
326
|
+
});
|
|
327
|
+
receive({
|
|
328
|
+
insertItem: () => {
|
|
329
|
+
run('INSERT INTO messages(msg_index,at,from_json,from_key_json,body_json,metadata_json,preview) VALUES(?,?,?,?,?,?,?)', [next, finite(draft.at), encode(from), key(from), encode(body), metadata === undefined ? null : encode(metadata), draft.preview ?? null]);
|
|
330
|
+
},
|
|
331
|
+
insertAddress: (index, { recipient, via }, order) => {
|
|
332
|
+
run('INSERT INTO message_addresses(msg_index,recipient_key_json,recipient_json,recipient_order,generation_json) VALUES(?,?,?,?,?)', [index, key(recipient), encode(recipient), order, via.generation === undefined ? null : encode(endpointGeneration(via.generation))]);
|
|
333
|
+
},
|
|
334
|
+
}, { item: { key: next, data: draft }, audience });
|
|
335
|
+
run('UPDATE channel_meta SET next_msg_index=? WHERE id=1', [next + 1]);
|
|
336
|
+
changed = true;
|
|
337
|
+
return message(one('SELECT * FROM messages WHERE msg_index=?', [next]));
|
|
338
|
+
}
|
|
339
|
+
function endpointMatches(current, expected) {
|
|
340
|
+
return current !== undefined && current.owner === expected.owner && current.generation === expected.generation;
|
|
341
|
+
}
|
|
342
|
+
function deleteEndpointClaims(name) {
|
|
343
|
+
run('DELETE FROM receive_claims WHERE owner_key_json=?', [encode(name)]);
|
|
344
|
+
run('DELETE FROM delivery_deferrals WHERE participant_key_json=?', [key(name)]);
|
|
345
|
+
}
|
|
346
|
+
const channelWrite = Object.assign({}, channelRead, {
|
|
347
|
+
publishMessage: writeMessage,
|
|
348
|
+
storeEndpoint(value) {
|
|
349
|
+
validateOwner(value.owner);
|
|
350
|
+
endpointGeneration(value.generation);
|
|
351
|
+
string(value.participant, true);
|
|
352
|
+
const existing = currentEndpoint(value.participant);
|
|
353
|
+
if (existing !== undefined && stableJson(existing) === stableJson(value))
|
|
354
|
+
return false;
|
|
355
|
+
run('INSERT INTO endpoints(participant_key_json,participant_json,owner_json,generation_json,wake_address_json,attachment) VALUES(?,?,?,?,?,?) ON CONFLICT(participant_key_json) DO UPDATE SET participant_json=excluded.participant_json,owner_json=excluded.owner_json,generation_json=excluded.generation_json,wake_address_json=excluded.wake_address_json,attachment=excluded.attachment', [key(value.participant), encode(value.participant), encode(value.owner), encode(value.generation), value.wakeAddress === undefined ? null : encode(value.wakeAddress), value.attachment]);
|
|
356
|
+
deleteEndpointClaims(value.participant);
|
|
357
|
+
deliveryChanged = true;
|
|
358
|
+
return true;
|
|
359
|
+
},
|
|
360
|
+
updateWakeAddress(value, wakeAddress) {
|
|
361
|
+
if (wakeAddress !== undefined) {
|
|
362
|
+
requireFact(isWakeRouteKind(wakeAddress.kind));
|
|
363
|
+
requireFact(Object.values(wakeAddress.address).every((item) => typeof item === 'string'));
|
|
364
|
+
}
|
|
365
|
+
const current = one('SELECT wake_address_json FROM endpoints WHERE participant_key_json=? AND generation_json=?', [key(value.participant), encode(value.generation)]);
|
|
366
|
+
if (stableJson(current.wake_address_json === null ? null : json(current.wake_address_json)) === stableJson(wakeAddress ?? null))
|
|
367
|
+
return false;
|
|
368
|
+
const count = run('UPDATE endpoints SET wake_address_json=? WHERE participant_key_json=? AND generation_json=?', [wakeAddress === undefined ? null : encode(wakeAddress), key(value.participant), encode(value.generation)]);
|
|
369
|
+
if (count)
|
|
370
|
+
deliveryChanged = true;
|
|
371
|
+
return count > 0;
|
|
372
|
+
},
|
|
373
|
+
reattachEndpoint(value, address) {
|
|
374
|
+
const current = currentEndpoint(value.participant);
|
|
375
|
+
if (current?.attachment !== 'detached' || !endpointMatches(current, value))
|
|
376
|
+
return false;
|
|
377
|
+
run("UPDATE endpoints SET attachment='attached',wake_address_json=? WHERE participant_key_json=? AND generation_json=?", [address === undefined ? null : encode(wakeAddress(address)), key(value.participant), encode(value.generation)]);
|
|
378
|
+
deliveryChanged = true;
|
|
379
|
+
return true;
|
|
380
|
+
},
|
|
381
|
+
detachEndpoint(value) {
|
|
382
|
+
const count = run("UPDATE endpoints SET attachment='detached',wake_address_json=NULL WHERE participant_key_json=? AND generation_json=? AND owner_json=? AND attachment='attached'", [key(value.participant), encode(value.generation), encode(value.owner)]);
|
|
383
|
+
if (!count)
|
|
384
|
+
return false;
|
|
385
|
+
deleteEndpointClaims(value.participant);
|
|
386
|
+
deliveryChanged = true;
|
|
387
|
+
return true;
|
|
388
|
+
},
|
|
389
|
+
removeEndpoint(value) {
|
|
390
|
+
const current = currentEndpoint(value.participant);
|
|
391
|
+
if (!endpointMatches(current, value))
|
|
392
|
+
return false;
|
|
393
|
+
run('DELETE FROM endpoints WHERE participant_key_json=? AND generation_json=? AND owner_json=?', [key(value.participant), encode(value.generation), encode(value.owner)]);
|
|
394
|
+
deleteEndpointClaims(value.participant);
|
|
395
|
+
deliveryChanged = true;
|
|
396
|
+
return true;
|
|
397
|
+
},
|
|
398
|
+
cancelThrough(value, through) {
|
|
399
|
+
endpointGeneration(value.generation);
|
|
400
|
+
integer(through, -1);
|
|
401
|
+
const prior = channelRead.cancelledThrough(value.participant, value.generation) ?? -1;
|
|
402
|
+
if (through <= prior)
|
|
403
|
+
return false;
|
|
404
|
+
run('INSERT INTO delivery_cancellations(participant_key_json,generation_json,through_index) VALUES(?,?,?) ON CONFLICT(participant_key_json,generation_json) DO UPDATE SET through_index=excluded.through_index', [key(value.participant), encode(value.generation), through]);
|
|
405
|
+
deliveryChanged = true;
|
|
406
|
+
return true;
|
|
407
|
+
},
|
|
408
|
+
markSeen(name, indexes, at) {
|
|
409
|
+
finite(at);
|
|
410
|
+
const next = integer(one('SELECT next_msg_index FROM channel_meta WHERE id=1').next_msg_index);
|
|
411
|
+
indexes.forEach(index => requireFact(integer(index) < next, 'receipt references an unassigned message index.'));
|
|
412
|
+
const canonical = owner(name);
|
|
413
|
+
string(canonical, true);
|
|
414
|
+
const count = recordConsumed({
|
|
415
|
+
insertReceipt: (ref, time) => run('INSERT OR IGNORE INTO receipts(owner_key_json,msg_index,at) VALUES(?,?,?)', [encode(ref.recipient), ref.item, time]) > 0,
|
|
416
|
+
}, indexes.map(item => ({ recipient: canonical, item })), at);
|
|
417
|
+
if (count)
|
|
418
|
+
changed = true;
|
|
419
|
+
return count;
|
|
420
|
+
},
|
|
421
|
+
putDeliveryDeferral(seatRef, id, match, expiresAt, now) {
|
|
422
|
+
finite(now);
|
|
423
|
+
finite(expiresAt);
|
|
424
|
+
const canonicalMatch = validateMetadataMatch(match);
|
|
425
|
+
run('DELETE FROM delivery_deferrals WHERE participant_key_json=? AND expires_at<=?', [key(seatRef.participant), now]);
|
|
426
|
+
run('INSERT INTO delivery_deferrals(participant_key_json,participant_json,generation_json,deferral_id_json,match_json,expires_at) VALUES(?,?,?,?,?,?)', [key(seatRef.participant), encode(seatRef.participant), encode(seatRef.generation), encode(id), encode(canonicalMatch), expiresAt]);
|
|
427
|
+
deliveryChanged = true;
|
|
428
|
+
return true;
|
|
429
|
+
},
|
|
430
|
+
renewDeliveryDeferral(seatRef, id, expiresAt, now) {
|
|
431
|
+
finite(now);
|
|
432
|
+
finite(expiresAt);
|
|
433
|
+
const pruned = run('DELETE FROM delivery_deferrals WHERE participant_key_json=? AND expires_at<=?', [key(seatRef.participant), now]);
|
|
434
|
+
const count = run('UPDATE delivery_deferrals SET expires_at=? WHERE participant_key_json=? AND generation_json=? AND deferral_id_json=? AND expires_at>?', [expiresAt, key(seatRef.participant), encode(seatRef.generation), encode(id), now]);
|
|
435
|
+
if (count || pruned)
|
|
436
|
+
deliveryChanged = true;
|
|
437
|
+
return count > 0;
|
|
438
|
+
},
|
|
439
|
+
releaseDeliveryDeferral(seatRef, id, now) {
|
|
440
|
+
finite(now);
|
|
441
|
+
const pruned = run('DELETE FROM delivery_deferrals WHERE participant_key_json=? AND expires_at<=?', [key(seatRef.participant), now]);
|
|
442
|
+
const count = run('DELETE FROM delivery_deferrals WHERE participant_key_json=? AND generation_json=? AND deferral_id_json=? AND expires_at>?', [key(seatRef.participant), encode(seatRef.generation), encode(id), now]);
|
|
443
|
+
if (count || pruned)
|
|
444
|
+
deliveryChanged = true;
|
|
445
|
+
return count > 0;
|
|
446
|
+
},
|
|
447
|
+
putReceiveClaim(claim) {
|
|
448
|
+
string(claim.namespace, true);
|
|
449
|
+
string(claim.id, true);
|
|
450
|
+
string(claim.owner, true);
|
|
451
|
+
endpointGeneration(claim.generation);
|
|
452
|
+
finite(claim.heartbeatAt);
|
|
453
|
+
finite(claim.expiresAt);
|
|
454
|
+
requireFact(claim.expiresAt >= claim.heartbeatAt);
|
|
455
|
+
requireFact(Array.isArray(claim.suppressed) && claim.suppressed.every(value => Number.isSafeInteger(value) && value >= 0), 'receive claim coverage is malformed.');
|
|
456
|
+
const selector = stableJson(claim.selector), suppressed = stableJson([...new Set(claim.suppressed)].sort((a, b) => a - b));
|
|
457
|
+
const previous = one('SELECT * FROM receive_claims WHERE namespace_key_json=? AND owner_key_json=?', [encode(claim.namespace), encode(claim.owner)]);
|
|
458
|
+
const prior = previous === undefined ? undefined : receiveClaim(previous);
|
|
459
|
+
if (prior !== undefined && prior.id === claim.id && stableJson(prior.selector) === selector
|
|
460
|
+
&& stableJson(prior.suppressed) === suppressed && prior.generation === claim.generation
|
|
461
|
+
&& prior.heartbeatAt === claim.heartbeatAt && prior.expiresAt === claim.expiresAt)
|
|
462
|
+
return false;
|
|
463
|
+
run('INSERT INTO receive_claims(namespace_key_json,owner_key_json,generation_json,claim_id_json,heartbeat_at,expires_at,selector_json,suppressed_json) VALUES(?,?,?,?,?,?,?,?) ON CONFLICT(namespace_key_json,owner_key_json) DO UPDATE SET generation_json=excluded.generation_json,claim_id_json=excluded.claim_id_json,heartbeat_at=excluded.heartbeat_at,expires_at=excluded.expires_at,selector_json=excluded.selector_json,suppressed_json=excluded.suppressed_json', [encode(claim.namespace), encode(claim.owner), encode(claim.generation), encode(claim.id), claim.heartbeatAt, claim.expiresAt, encode(claim.selector), suppressed]);
|
|
464
|
+
changed = true;
|
|
465
|
+
return true;
|
|
466
|
+
},
|
|
467
|
+
removeReceiveClaim(ownerName, namespace, id) {
|
|
468
|
+
const count = run('DELETE FROM receive_claims WHERE namespace_key_json=? AND owner_key_json=? AND claim_id_json=?', [encode(namespace), encode(ownerName), encode(id)]);
|
|
469
|
+
if (count)
|
|
470
|
+
changed = true;
|
|
471
|
+
return count > 0;
|
|
472
|
+
},
|
|
473
|
+
});
|
|
474
|
+
Object.assign(channel, channelWrite);
|
|
475
|
+
const lower = channelRead;
|
|
476
|
+
function attempt(row) {
|
|
477
|
+
const data = object(json(row.payload_json)), evidence = deliveryEvidence(row, data);
|
|
478
|
+
return { purpose: evidence.kind === 'wake' ? 'dispatch' : 'presentation', token: evidence.claimToken ?? '',
|
|
479
|
+
ordinal: evidence.attemptN, outcome: evidence.outcome, at: evidence.at, expiresAt: evidence.expiresAt, data };
|
|
480
|
+
}
|
|
481
|
+
function attemptRows(seat, index, kind, known) {
|
|
482
|
+
const scope = [key(seat.participant), encode(seat.generation), index, kind];
|
|
483
|
+
let sources = [];
|
|
484
|
+
return {
|
|
485
|
+
list() { sources = all('SELECT * FROM delivery_evidence WHERE participant_key_json=? AND generation_json=? AND msg_index=? AND kind=? ORDER BY attempt_n DESC', scope); return sources.map(attempt); },
|
|
486
|
+
evidence(value) { const source = sources.find(row => row.attempt_n === value.ordinal); return deliveryEvidence({ ...source, outcome: value.outcome, at: value.at, expires_at: value.expiresAt ?? null }, value.data); },
|
|
487
|
+
byToken(token) {
|
|
488
|
+
if (known?.token === token)
|
|
489
|
+
return known;
|
|
490
|
+
const row = one('SELECT * FROM delivery_evidence WHERE participant_key_json=? AND generation_json=? AND msg_index=? AND kind=? AND claim_token_json=?', [...scope, encode(token)]);
|
|
491
|
+
return row === undefined ? undefined : attempt(row);
|
|
492
|
+
},
|
|
493
|
+
insert(value) {
|
|
494
|
+
run('INSERT INTO delivery_evidence(participant_key_json,participant_json,generation_json,msg_index,kind,attempt_n,outcome,at,expires_at,claim_token_json,payload_json) VALUES(?,?,?,?,?,?,?,?,?,?,?)', [scope[0], encode(seat.participant), scope[1], index, kind, value.ordinal, value.outcome, value.at, value.expiresAt ?? null, encode(value.token), encode(value.data)]);
|
|
495
|
+
deliveryChanged = true;
|
|
496
|
+
},
|
|
497
|
+
replace(value) {
|
|
498
|
+
const count = run('UPDATE delivery_evidence SET outcome=?,at=?,expires_at=?,payload_json=? WHERE participant_key_json=? AND generation_json=? AND msg_index=? AND kind=? AND attempt_n=? AND claim_token_json IS ?', [value.outcome, value.at, value.expiresAt ?? null, encode(value.data), ...scope, value.ordinal, value.token === '' ? null : encode(value.token)]);
|
|
499
|
+
if (count)
|
|
500
|
+
deliveryChanged = true;
|
|
501
|
+
},
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
const delivery = {
|
|
505
|
+
claimDelivery(input) {
|
|
506
|
+
requireFact(input.kind === 'wake' || input.kind === 'presentation');
|
|
507
|
+
const index = parseMessageId(input.message);
|
|
508
|
+
requireFact(index !== undefined, 'message id is malformed.');
|
|
509
|
+
finite(input.at);
|
|
510
|
+
const leaseMs = integer(input.leaseMs, 1);
|
|
511
|
+
string(input.claimToken, true);
|
|
512
|
+
if (input.route !== undefined)
|
|
513
|
+
wakeAddress(input.route);
|
|
514
|
+
const currentSeat = currentEndpoint(input.seat.participant);
|
|
515
|
+
if (currentSeat === undefined || currentSeat.attachment !== 'attached' || currentSeat.generation !== input.seat.generation || currentSeat.owner !== input.seat.owner
|
|
516
|
+
|| lower.directed(input.seat.participant, { afterIndex: index - 1, throughIndex: index, currentGeneration: true }, { edge: 'oldest', limit: 1 }).items.length === 0)
|
|
517
|
+
return { status: 'stale' };
|
|
518
|
+
const metadata = lower.message(index)?.metadata;
|
|
519
|
+
if (matchingDeferrals(lower.deliveryDeferrals(currentSeat.participant, currentSeat.generation), metadata, input.at).length > 0)
|
|
520
|
+
return { status: 'deferred' };
|
|
521
|
+
const cancelledThrough = lower.cancelledThrough(input.seat.participant, input.seat.generation);
|
|
522
|
+
if (cancelledThrough !== undefined && index <= cancelledThrough)
|
|
523
|
+
return { status: 'stale' };
|
|
524
|
+
const existing = lower.deliveryEvidence(input.seat.participant, input.seat.generation, formatMessageId(index));
|
|
525
|
+
if (input.observationOf !== undefined) {
|
|
526
|
+
if (input.kind !== 'presentation' || !existing.some(row => row.kind === 'wake'
|
|
527
|
+
&& row.claimToken === input.observationOf && ['dispatching', 'accepted', 'unknown'].includes(row.outcome)))
|
|
528
|
+
return { status: 'stale' };
|
|
529
|
+
}
|
|
530
|
+
else {
|
|
531
|
+
const blocker = existing.find(row => row.kind !== input.kind && row.outcome !== 'failed' && row.outcome !== 'released');
|
|
532
|
+
if (blocker !== undefined) {
|
|
533
|
+
if (blocker.outcome === 'dispatching' && blocker.expiresAt !== undefined && blocker.expiresAt <= input.at) {
|
|
534
|
+
// Normalize abandoned custody to unknown in the same transaction, never steal it.
|
|
535
|
+
const rows = attemptRows(input.seat, index, blocker.kind);
|
|
536
|
+
const normalized = { ...blocker, outcome: 'unknown', at: input.at, expiresAt: undefined };
|
|
537
|
+
rows.replace({ purpose: blocker.kind === 'wake' ? 'dispatch' : 'presentation', token: blocker.claimToken ?? '', ordinal: blocker.attemptN,
|
|
538
|
+
outcome: 'unknown', at: input.at, data: {} });
|
|
539
|
+
return { status: 'terminal', evidence: normalized };
|
|
540
|
+
}
|
|
541
|
+
return { status: blocker.outcome === 'dispatching' ? 'busy' : 'terminal', evidence: blocker };
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
const rows = attemptRows(input.seat, index, input.kind);
|
|
545
|
+
const result = claimAttempt({ ...rows, replace(value) { rows.replace({ ...value, data: { ...value.data, diagnostic: 'interrupted dispatch' } }); } }, { purpose: input.kind === 'wake' ? 'dispatch' : 'presentation', token: input.claimToken, at: input.at, leaseMs, data: input.route === undefined ? {} : { route: input.route } });
|
|
546
|
+
if (result.status === 'acquired')
|
|
547
|
+
return { status: 'acquired', claimToken: result.token, attemptN: result.ordinal };
|
|
548
|
+
const evidence = result.interrupted ? { ...result.evidence, data: { ...result.evidence.data, diagnostic: 'interrupted dispatch' } } : result.evidence;
|
|
549
|
+
return { status: result.status, evidence: rows.evidence(evidence) };
|
|
550
|
+
},
|
|
551
|
+
prepareDelivery(input) {
|
|
552
|
+
const row = one('SELECT * FROM delivery_evidence WHERE participant_key_json=? AND generation_json=? AND claim_token_json=?', [key(input.seat.participant), encode(input.seat.generation), encode(input.claimToken)]);
|
|
553
|
+
if (row === undefined || row.outcome !== 'dispatching')
|
|
554
|
+
return false;
|
|
555
|
+
requireFact(input.coverage === 'full' || input.coverage === 'clipped');
|
|
556
|
+
const previous = attempt(row), correlation = nativeCorrelation(input.correlation), payload = previous.data;
|
|
557
|
+
if (stableJson(payload.correlation ?? null) === stableJson(correlation) && payload.coverage === input.coverage
|
|
558
|
+
&& stableJson(payload.description ?? null) === stableJson(input.description ?? null))
|
|
559
|
+
return true;
|
|
560
|
+
return prepareAttempt(attemptRows(input.seat, integer(row.msg_index), row.kind, previous), { token: input.claimToken, data: { ...payload, correlation, coverage: input.coverage, ...(input.description === undefined ? {} : { description: input.description }) } });
|
|
561
|
+
},
|
|
562
|
+
completeDelivery(input) {
|
|
563
|
+
const index = parseMessageId(input.message);
|
|
564
|
+
requireFact(index !== undefined, 'message id is malformed.');
|
|
565
|
+
finite(input.at);
|
|
566
|
+
requireFact(['accepted', 'failed', 'unknown', 'released', 'presented', 'clipped'].includes(input.outcome));
|
|
567
|
+
string(input.claimToken, true);
|
|
568
|
+
const row = one('SELECT * FROM delivery_evidence WHERE participant_key_json=? AND generation_json=? AND msg_index=? AND claim_token_json=?', [key(input.seat.participant), encode(input.seat.generation), index, encode(input.claimToken)]);
|
|
569
|
+
if (row === undefined)
|
|
570
|
+
return false;
|
|
571
|
+
const previous = attempt(row);
|
|
572
|
+
const payload = { ...previous.data, ...(input.signature === undefined ? {} : { signature: input.signature }), ...(input.messageText === undefined ? {} : { message: input.messageText }), ...(input.diagnostic === undefined ? {} : { diagnostic: input.diagnostic }), ...(input.correlation === undefined ? {} : { correlation: input.correlation }) };
|
|
573
|
+
return settleAttempt(attemptRows(input.seat, index, row.kind, previous), { token: input.claimToken, outcome: input.outcome, at: input.at, data: payload });
|
|
574
|
+
},
|
|
575
|
+
observeNativeDelivery(input) {
|
|
576
|
+
const row = one('SELECT * FROM delivery_evidence WHERE participant_key_json=? AND generation_json=? AND claim_token_json=?', [key(input.seat.participant), encode(input.seat.generation), encode(input.claimToken)]);
|
|
577
|
+
if (row === undefined || !['dispatching', 'accepted', 'unknown'].includes(string(row.outcome)))
|
|
578
|
+
return false;
|
|
579
|
+
const observation = input.observation;
|
|
580
|
+
requireFact(typeof observation.eventId === 'string' && observation.eventId.length > 0);
|
|
581
|
+
requireFact(typeof observation.messageId === 'string' && observation.messageId.length > 0);
|
|
582
|
+
requireFact(observation.state === 'held' || observation.state === 'released' || observation.state === 'dropped');
|
|
583
|
+
finite(observation.observedAt);
|
|
584
|
+
if (observation.reason !== undefined)
|
|
585
|
+
string(observation.reason);
|
|
586
|
+
const payload = object(json(row.payload_json));
|
|
587
|
+
const observations = payload.nativeObservations === undefined ? [] : payload.nativeObservations;
|
|
588
|
+
requireFact(Array.isArray(observations));
|
|
589
|
+
if (observations.some((item) => object(item).eventId === observation.eventId))
|
|
590
|
+
return true;
|
|
591
|
+
const next = [...observations, observation];
|
|
592
|
+
const count = run('UPDATE delivery_evidence SET payload_json=? WHERE participant_key_json=? AND generation_json=? AND claim_token_json=?', [encode({ ...payload, nativeObservations: next }), key(input.seat.participant), encode(input.seat.generation), encode(input.claimToken)]);
|
|
593
|
+
if (count)
|
|
594
|
+
deliveryChanged = true;
|
|
595
|
+
return count > 0;
|
|
596
|
+
},
|
|
597
|
+
};
|
|
598
|
+
Object.assign(channel, delivery);
|
|
599
|
+
return Object.defineProperties(channel, { changed: { get: () => changed }, deliveryChanged: { get: () => deliveryChanged } });
|
|
600
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { DatabaseSync } from 'node:sqlite';
|
|
2
|
+
/** The only schema owned by the shared physical store. */
|
|
3
|
+
export declare const STORAGE_META_SQL = "CREATE TABLE storage_meta (\n id INTEGER PRIMARY KEY CHECK (id = 1),\n revision INTEGER NOT NULL CHECK (revision BETWEEN 0 AND 9007199254740991),\n delivery_revision INTEGER NOT NULL CHECK (delivery_revision BETWEEN 0 AND 9007199254740991)\n);";
|
|
4
|
+
export declare function initializeStorageMeta(db: DatabaseSync, revision?: number): void;
|
|
5
|
+
export declare function resetStorageMeta(db: DatabaseSync): void;
|
|
6
|
+
export declare function readStorageRevision(db: DatabaseSync, delivery?: boolean): number;
|
|
7
|
+
/** Compare an owner's complete current schema and journal mode without modifying the database. */
|
|
8
|
+
export declare function validateSchema(db: DatabaseSync, expectedSql: string, location?: string): void;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { integer, requireFact, string } from './sqlite-values.js';
|
|
2
|
+
/** The only schema owned by the shared physical store. */
|
|
3
|
+
export const STORAGE_META_SQL = `CREATE TABLE storage_meta (
|
|
4
|
+
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
5
|
+
revision INTEGER NOT NULL CHECK (revision BETWEEN 0 AND 9007199254740991),
|
|
6
|
+
delivery_revision INTEGER NOT NULL CHECK (delivery_revision BETWEEN 0 AND 9007199254740991)
|
|
7
|
+
);`;
|
|
8
|
+
export function initializeStorageMeta(db, revision = 0) {
|
|
9
|
+
integer(revision);
|
|
10
|
+
db.exec(STORAGE_META_SQL);
|
|
11
|
+
db.prepare('INSERT INTO storage_meta(id,revision,delivery_revision) VALUES(1,?,0)').run(revision);
|
|
12
|
+
}
|
|
13
|
+
export function resetStorageMeta(db) {
|
|
14
|
+
db.exec('DROP TABLE storage_meta');
|
|
15
|
+
}
|
|
16
|
+
export function readStorageRevision(db, delivery = false) {
|
|
17
|
+
const rows = db.prepare(`SELECT id, ${delivery ? 'delivery_revision' : 'revision'} AS revision FROM storage_meta`).all();
|
|
18
|
+
requireFact(rows.length === 1 && rows[0].id === 1, 'revision header is malformed.');
|
|
19
|
+
return integer(rows[0].revision);
|
|
20
|
+
}
|
|
21
|
+
function normalizedSql(sql) {
|
|
22
|
+
return sql.trim().replace(/;$/, '').replace(/\s+/g, ' ').replace(/\s*([(),=])\s*/g, '$1');
|
|
23
|
+
}
|
|
24
|
+
/** Compare an owner's complete current schema and journal mode without modifying the database. */
|
|
25
|
+
export function validateSchema(db, expectedSql, location = '') {
|
|
26
|
+
const expected = expectedSql.split(';').map(s => s.trim()).filter(Boolean).map(normalizedSql).sort();
|
|
27
|
+
const actual = db.prepare("SELECT sql FROM sqlite_schema WHERE name NOT GLOB 'sqlite_*' AND sql IS NOT NULL").all()
|
|
28
|
+
.map(row => normalizedSql(string(row.sql))).sort();
|
|
29
|
+
requireFact(actual.length === expected.length && actual.every((sql, index) => sql === expected[index]), 'unsupported row schema.');
|
|
30
|
+
requireFact(db.prepare('PRAGMA journal_mode').get().journal_mode === (location === ':memory:' ? 'memory' : 'delete'), 'unsupported journal mode.');
|
|
31
|
+
}
|