@astrosheep/square 0.3.106 → 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/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 +1 -1
- 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
package/dist/square-sqlite.js
CHANGED
|
@@ -1,161 +1,47 @@
|
|
|
1
1
|
import { DatabaseSync as MemoryDatabase } from 'node:sqlite';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { deriveLanding, landingFacts } from './square-domain.js';
|
|
5
|
-
import { claimAttempt, prepareAttempt, settleAttempt } from './delivery-attempts.js';
|
|
6
|
-
import { legacyRowFramingIssue, legacyActivitySchema } from './square-legacy-framing.js';
|
|
7
|
-
import { receive, recordConsumed } from './receive.js';
|
|
2
|
+
import { validateActivityMetadata, activityMetadataHasContent } from './model.js';
|
|
3
|
+
import { catchCoverage, deriveLanding, landingFacts, extendLandingCatchCoverage } from './square-domain.js';
|
|
8
4
|
import { formatActivityId } from './square-core.js';
|
|
9
5
|
import { compileSearchPattern } from './search.js';
|
|
6
|
+
import { createChannelSqlite, endpoint, endpointGeneration, messageAddress, deliveryEvidence, deliveryDeferral, receiveClaim } from './sqlite-channel.js';
|
|
7
|
+
import { readStorageRevision, initializeStorageMeta, STORAGE_META_SQL, validateSchema } from './sqlite-framing.js';
|
|
8
|
+
import { encode, key, invalidArtifact, requireFact, integer, finite, string, json, object, shape, strings, decoded, immutable, stableJson, pageRows, pageLimit } from './sqlite-values.js';
|
|
9
|
+
export { invalidArtifact } from './sqlite-values.js';
|
|
10
|
+
export { endpointGeneration } from './sqlite-channel.js';
|
|
11
|
+
export const APPLICATION_ID = 0x53515245;
|
|
12
|
+
export const CURRENT_USER_VERSION = 11;
|
|
10
13
|
export function emptyRuntimeState(nextActIndex = 0) {
|
|
11
14
|
return { nextActIndex, observations: {}, leases: {} };
|
|
12
15
|
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
export const
|
|
16
|
-
export const V3_USER_VERSION = 3;
|
|
17
|
-
export const V4_USER_VERSION = 4;
|
|
18
|
-
export const V5_USER_VERSION = 5;
|
|
19
|
-
export const V6_USER_VERSION = 6;
|
|
20
|
-
export const V7_USER_VERSION = 7;
|
|
21
|
-
export const CURRENT_USER_VERSION = 8;
|
|
22
|
-
export const V1_SNAPSHOT_TABLE = 'square_snapshot';
|
|
23
|
-
const V1_SNAPSHOT_DDL = `CREATE TABLE ${V1_SNAPSHOT_TABLE} (id INTEGER PRIMARY KEY CHECK(id=1), revision INTEGER NOT NULL CHECK(revision>=0), state TEXT NOT NULL)`;
|
|
24
|
-
const V1_SNAPSHOT_COLUMNS = [
|
|
25
|
-
{ cid: 0, name: 'id', type: 'INTEGER', notnull: 0, dflt_value: null, pk: 1, hidden: 0 },
|
|
26
|
-
{ cid: 1, name: 'revision', type: 'INTEGER', notnull: 1, dflt_value: null, pk: 0, hidden: 0 },
|
|
27
|
-
{ cid: 2, name: 'state', type: 'TEXT', notnull: 1, dflt_value: null, pk: 0, hidden: 0 },
|
|
28
|
-
];
|
|
29
|
-
/**
|
|
30
|
-
* Read-only legacy framing: application id, format version, journal mode and the
|
|
31
|
-
* exact owned snapshot schema. It decodes no snapshot payload, writes no pragma
|
|
32
|
-
* and runs no DDL, so ordinary open can establish ownership before requiring
|
|
33
|
-
* migration. Anything else is unrelated or unsupported, not repaired.
|
|
34
|
-
*/
|
|
35
|
-
export function validateLegacyFraming(database, applicationId, location = '') {
|
|
36
|
-
const identity = database.prepare('SELECT application_id, user_version FROM pragma_application_id, pragma_user_version').get();
|
|
37
|
-
if (identity.application_id !== applicationId)
|
|
38
|
-
throw invalidArtifact('unrelated SQLite database.');
|
|
39
|
-
if (identity.user_version !== V1_USER_VERSION)
|
|
40
|
-
throw invalidArtifact('unsupported SQLite format version.');
|
|
41
|
-
const journalMode = database.prepare('PRAGMA journal_mode').get().journal_mode;
|
|
42
|
-
if (typeof journalMode !== 'string' || journalMode !== (location === ':memory:' ? 'memory' : 'delete'))
|
|
43
|
-
throw invalidArtifact('unsupported journal mode.');
|
|
44
|
-
const objects = database.prepare("SELECT type, name, tbl_name, sql FROM sqlite_schema WHERE name NOT GLOB 'sqlite_*' ORDER BY type, name")
|
|
45
|
-
.all();
|
|
46
|
-
if (objects.length !== 1)
|
|
47
|
-
throw invalidArtifact('unsupported snapshot schema.');
|
|
48
|
-
const [object] = objects;
|
|
49
|
-
if (object.type !== 'table' || object.name !== V1_SNAPSHOT_TABLE || object.tbl_name !== V1_SNAPSHOT_TABLE
|
|
50
|
-
|| typeof object.sql !== 'string' || normalizedLegacySql(object.sql) !== normalizedLegacySql(V1_SNAPSHOT_DDL))
|
|
51
|
-
throw invalidArtifact('unsupported snapshot schema.');
|
|
52
|
-
const columns = database.prepare('SELECT cid, name, type, "notnull", dflt_value, pk, hidden FROM pragma_table_xinfo(?) ORDER BY cid')
|
|
53
|
-
.all(V1_SNAPSHOT_TABLE);
|
|
54
|
-
if (columns.length !== V1_SNAPSHOT_COLUMNS.length || !columns.every((column, index) => {
|
|
55
|
-
const expected = V1_SNAPSHOT_COLUMNS[index];
|
|
56
|
-
return column.cid === expected.cid && column.name === expected.name && column.type === expected.type
|
|
57
|
-
&& column.notnull === expected.notnull && column.dflt_value === expected.dflt_value
|
|
58
|
-
&& column.pk === expected.pk && column.hidden === expected.hidden;
|
|
59
|
-
}))
|
|
60
|
-
throw invalidArtifact('unsupported snapshot schema.');
|
|
61
|
-
return { applicationId, userVersion: V1_USER_VERSION, journalMode };
|
|
62
|
-
}
|
|
63
|
-
function normalizedLegacySql(sql) {
|
|
64
|
-
return sql.trim().replace(/\s+/g, ' ').replace(/\s*(>=|[(),=])\s*/g, '$1');
|
|
65
|
-
}
|
|
66
|
-
const CURRENT_ACTIVITY_KIND_CHECK = "kind IN ('join','done','say','read','listen','ignore','unlisten')";
|
|
67
|
-
export const ACTIVITIES_TABLE_SQL = `CREATE TABLE activities (
|
|
68
|
-
act_index INTEGER PRIMARY KEY CHECK (act_index BETWEEN 0 AND 9007199254740991),
|
|
69
|
-
at REAL NOT NULL,
|
|
70
|
-
kind TEXT NOT NULL CHECK (${CURRENT_ACTIVITY_KIND_CHECK}),
|
|
71
|
-
actor_json TEXT COLLATE BINARY NOT NULL,
|
|
72
|
-
actor_key_json TEXT COLLATE BINARY NOT NULL,
|
|
73
|
-
reach TEXT CHECK (reach = 'bell'),
|
|
74
|
-
reply_index INTEGER CHECK (reply_index >= 0 AND reply_index < act_index),
|
|
75
|
-
payload_json TEXT NOT NULL CHECK (json_valid(payload_json)),
|
|
76
|
-
CHECK (kind = 'say' OR (reach IS NULL AND reply_index IS NULL))
|
|
77
|
-
);`;
|
|
78
|
-
export const ACTIVITY_INDEXES_SQL = [
|
|
79
|
-
'CREATE INDEX activity_kind_index ON activities(kind, act_index)',
|
|
80
|
-
'CREATE INDEX activity_actor_index ON activities(actor_key_json, act_index)',
|
|
81
|
-
"CREATE INDEX activity_say_actor_index ON activities(actor_key_json, act_index) WHERE kind='say'",
|
|
82
|
-
"CREATE INDEX activity_say_time ON activities(at, act_index) WHERE kind='say'",
|
|
83
|
-
"CREATE INDEX activity_bell_actor_time ON activities(actor_key_json, at, act_index) WHERE kind='say' AND reach='bell'",
|
|
84
|
-
];
|
|
85
|
-
export const DELIVERY_DEFERRALS_SQL = `CREATE TABLE delivery_deferrals (
|
|
86
|
-
participant_key_json TEXT COLLATE BINARY NOT NULL,
|
|
87
|
-
participant_json TEXT NOT NULL,
|
|
88
|
-
generation_json TEXT NOT NULL,
|
|
89
|
-
deferral_id_json TEXT NOT NULL,
|
|
90
|
-
match_json TEXT NOT NULL CHECK (json_valid(match_json)),
|
|
91
|
-
expires_at REAL NOT NULL,
|
|
92
|
-
PRIMARY KEY (participant_key_json,generation_json,deferral_id_json)
|
|
93
|
-
) WITHOUT ROWID;
|
|
94
|
-
CREATE INDEX delivery_deferral_expiry ON delivery_deferrals(participant_key_json,generation_json,expires_at);`;
|
|
95
|
-
const ATTACHMENT_COLUMN_SQL = "attachment TEXT NOT NULL DEFAULT 'attached' CHECK (attachment IN ('attached','detached'))";
|
|
96
|
-
export const SCHEMA_SQL = `CREATE TABLE square_meta (
|
|
16
|
+
const ACTIVITY_KIND_CHECK = "kind IN ('join','done','say','read','listen','ignore','unlisten')";
|
|
17
|
+
/** Agora owns only its allocator/configuration header and social tables; neutral storage owns the revision header. */
|
|
18
|
+
export const AGORA_SCHEMA_SQL = `CREATE TABLE square_meta (
|
|
97
19
|
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
98
|
-
revision INTEGER NOT NULL CHECK (revision BETWEEN 0 AND 9007199254740991),
|
|
99
|
-
delivery_revision INTEGER NOT NULL CHECK (delivery_revision BETWEEN 0 AND 9007199254740991),
|
|
100
20
|
next_act_index INTEGER NOT NULL CHECK (next_act_index BETWEEN 0 AND 9007199254740991),
|
|
101
21
|
hard_cap INTEGER CHECK (hard_cap BETWEEN 1 AND 9007199254740991),
|
|
102
22
|
throttle_per_minute INTEGER CHECK (throttle_per_minute BETWEEN 1 AND 9007199254740991),
|
|
103
23
|
preamble_json TEXT NOT NULL CHECK (json_valid(preamble_json)),
|
|
104
24
|
warmup_json TEXT NOT NULL CHECK (json_valid(warmup_json))
|
|
105
25
|
);
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
CREATE TABLE receipts (
|
|
110
|
-
owner_json TEXT COLLATE BINARY NOT NULL,
|
|
111
|
-
act_index INTEGER NOT NULL CHECK (act_index BETWEEN 0 AND 9007199254740991),
|
|
112
|
-
at REAL NOT NULL,
|
|
113
|
-
PRIMARY KEY (owner_json, act_index)
|
|
114
|
-
) WITHOUT ROWID;
|
|
115
|
-
CREATE INDEX receipt_owner_time ON receipts(owner_json, at);
|
|
116
|
-
CREATE TABLE catch_leases (
|
|
117
|
-
owner_json TEXT COLLATE BINARY PRIMARY KEY NOT NULL,
|
|
118
|
-
generation_json TEXT NOT NULL DEFAULT '""',
|
|
119
|
-
lease_id_json TEXT NOT NULL,
|
|
120
|
-
heartbeat_at REAL NOT NULL,
|
|
121
|
-
expires_at REAL NOT NULL CHECK (expires_at >= heartbeat_at),
|
|
122
|
-
filter_json TEXT CHECK (filter_json IS NULL OR json_valid(filter_json))
|
|
123
|
-
) WITHOUT ROWID;
|
|
124
|
-
|
|
125
|
-
CREATE TABLE seats (
|
|
126
|
-
participant_key_json TEXT COLLATE BINARY PRIMARY KEY NOT NULL,
|
|
127
|
-
participant_json TEXT NOT NULL,
|
|
128
|
-
owner_json TEXT NOT NULL,
|
|
129
|
-
generation_json TEXT NOT NULL,
|
|
130
|
-
wake_address_json TEXT CHECK (wake_address_json IS NULL OR json_valid(wake_address_json)),
|
|
131
|
-
${ATTACHMENT_COLUMN_SQL}
|
|
132
|
-
) WITHOUT ROWID;
|
|
133
|
-
CREATE INDEX seat_owner ON seats(owner_json);
|
|
134
|
-
CREATE TABLE delivery_evidence (
|
|
135
|
-
participant_key_json TEXT COLLATE BINARY NOT NULL,
|
|
136
|
-
participant_json TEXT NOT NULL,
|
|
137
|
-
generation_json TEXT NOT NULL,
|
|
138
|
-
act_index INTEGER NOT NULL CHECK (act_index BETWEEN 0 AND 9007199254740991),
|
|
139
|
-
kind TEXT NOT NULL CHECK (kind IN ('wake','presentation')),
|
|
140
|
-
attempt_n INTEGER NOT NULL CHECK (attempt_n >= 0),
|
|
141
|
-
outcome TEXT NOT NULL CHECK (outcome IN ('dispatching','accepted','failed','unknown','released','presented','clipped')),
|
|
26
|
+
CREATE TABLE activities (
|
|
27
|
+
act_index INTEGER PRIMARY KEY CHECK (act_index BETWEEN 0 AND 9007199254740991),
|
|
142
28
|
at REAL NOT NULL,
|
|
143
|
-
|
|
144
|
-
|
|
29
|
+
kind TEXT NOT NULL CHECK (${ACTIVITY_KIND_CHECK}),
|
|
30
|
+
actor_json TEXT COLLATE BINARY NOT NULL,
|
|
31
|
+
actor_key_json TEXT COLLATE BINARY NOT NULL,
|
|
32
|
+
reach TEXT CHECK (reach = 'bell'),
|
|
33
|
+
reply_index INTEGER CHECK (reply_index >= 0 AND reply_index < act_index),
|
|
34
|
+
message_index INTEGER CHECK (message_index IS NULL OR message_index BETWEEN 0 AND 9007199254740991),
|
|
145
35
|
payload_json TEXT NOT NULL CHECK (json_valid(payload_json)),
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
CREATE INDEX
|
|
150
|
-
CREATE
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
) WITHOUT ROWID;
|
|
156
|
-
|
|
157
|
-
${DELIVERY_DEFERRALS_SQL}
|
|
158
|
-
|
|
36
|
+
CHECK ((kind = 'say') = (message_index IS NOT NULL)),
|
|
37
|
+
CHECK (kind = 'say' OR (reach IS NULL AND reply_index IS NULL))
|
|
38
|
+
);
|
|
39
|
+
CREATE INDEX activity_kind_index ON activities(kind, act_index);
|
|
40
|
+
CREATE INDEX activity_actor_index ON activities(actor_key_json, act_index);
|
|
41
|
+
CREATE INDEX activity_say_actor_index ON activities(actor_key_json, act_index) WHERE kind='say';
|
|
42
|
+
CREATE INDEX activity_say_time ON activities(at, act_index) WHERE kind='say';
|
|
43
|
+
CREATE INDEX activity_bell_actor_time ON activities(actor_key_json, at, act_index) WHERE kind='say' AND reach='bell';
|
|
44
|
+
CREATE UNIQUE INDEX activity_message ON activities(message_index) WHERE message_index IS NOT NULL;
|
|
159
45
|
CREATE TABLE participants (
|
|
160
46
|
name_key_json TEXT COLLATE BINARY PRIMARY KEY NOT NULL,
|
|
161
47
|
name_json TEXT NOT NULL,
|
|
@@ -180,74 +66,33 @@ CREATE TABLE landed_audience (
|
|
|
180
66
|
act_index INTEGER NOT NULL,
|
|
181
67
|
recipient_key_json TEXT COLLATE BINARY NOT NULL,
|
|
182
68
|
recipient_order INTEGER NOT NULL CHECK (recipient_order >= 0),
|
|
183
|
-
|
|
69
|
+
reason TEXT NOT NULL CHECK (reason IN ('mention','reply','attention','bell')),
|
|
184
70
|
PRIMARY KEY (act_index, recipient_key_json),
|
|
185
71
|
UNIQUE (act_index, recipient_order)
|
|
186
72
|
) WITHOUT ROWID;
|
|
187
73
|
CREATE INDEX audience_recipient ON landed_audience(recipient_key_json, act_index);`;
|
|
188
|
-
export
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
const FORMAT7_SCHEMA = FORMAT7_SCHEMA_SQL.split(';').map(s => s.trim()).filter(Boolean).map(normalizedSql).sort();
|
|
193
|
-
const FORMAT6_SCHEMA = FORMAT6_SCHEMA_SQL.split(';').map(s => s.trim()).filter(Boolean).map(normalizedSql).sort();
|
|
194
|
-
const FORMAT5_SCHEMA = FORMAT5_SCHEMA_SQL.split(';').map(s => s.trim()).filter(Boolean).map(normalizedSql).sort();
|
|
195
|
-
const EXPECTED_SCHEMA_SQL = SCHEMA_SQL.split(';').map(s => s.trim()).filter(Boolean).map(normalizedSql).sort();
|
|
196
|
-
const FORMAT4_SCHEMA = FORMAT4_SCHEMA_SQL.split(';').map(s => s.trim()).filter(Boolean).map(normalizedSql).sort();
|
|
197
|
-
const encode = JSON.stringify;
|
|
198
|
-
const key = (name) => encode(nameKey(name));
|
|
199
|
-
export function invalidArtifact(detail) {
|
|
200
|
-
return new SquareError('invalid_args', `Invalid square artifact: ${detail}`);
|
|
201
|
-
}
|
|
202
|
-
function requireFact(ok, detail = 'row is malformed.') {
|
|
203
|
-
if (!ok)
|
|
204
|
-
throw invalidArtifact(detail);
|
|
74
|
+
export function initializeAgoraSqlite(db, configuration = {}) {
|
|
75
|
+
db.exec(AGORA_SCHEMA_SQL);
|
|
76
|
+
db.prepare('INSERT INTO square_meta(id,next_act_index,hard_cap,throttle_per_minute,preamble_json,warmup_json) VALUES(1,0,?,?,?,?)')
|
|
77
|
+
.run(configuration.hardCap ?? null, configuration.throttlePerMinute ?? null, JSON.stringify(configuration.preamble ?? []), JSON.stringify(configuration.warmup ?? []));
|
|
205
78
|
}
|
|
206
|
-
function
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
}
|
|
210
|
-
function finite(value) {
|
|
211
|
-
requireFact(typeof value === 'number' && Number.isFinite(value));
|
|
212
|
-
return value;
|
|
213
|
-
}
|
|
214
|
-
function string(value, nonempty = false) {
|
|
215
|
-
requireFact(typeof value === 'string' && (!nonempty || value.length > 0));
|
|
216
|
-
return value;
|
|
217
|
-
}
|
|
218
|
-
function json(value) {
|
|
219
|
-
try {
|
|
220
|
-
return JSON.parse(string(value));
|
|
221
|
-
}
|
|
222
|
-
catch {
|
|
223
|
-
throw invalidArtifact('row contains invalid JSON.');
|
|
224
|
-
}
|
|
79
|
+
export function resetAgoraSqlite(db) {
|
|
80
|
+
for (const table of ['landed_audience', 'attention', 'participants', 'activities', 'square_meta'])
|
|
81
|
+
db.exec(`DROP TABLE ${table}`);
|
|
225
82
|
}
|
|
226
|
-
function
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
const result = object(value);
|
|
232
|
-
requireFact(required.every(k => Object.hasOwn(result, k)) && Object.keys(result).every(k => required.includes(k) || optional.includes(k)));
|
|
233
|
-
return result;
|
|
234
|
-
}
|
|
235
|
-
function strings(value) {
|
|
236
|
-
requireFact(Array.isArray(value) && value.every(v => typeof v === 'string'));
|
|
237
|
-
return value;
|
|
238
|
-
}
|
|
239
|
-
function decoded(value, nonempty = false) {
|
|
240
|
-
const result = string(json(value), nonempty);
|
|
241
|
-
requireFact(encode(result) === value, 'string encoding is malformed.');
|
|
242
|
-
return result;
|
|
83
|
+
function validateSquareFraming(db, lower, location = '') {
|
|
84
|
+
const identity = db.prepare('SELECT application_id, user_version FROM pragma_application_id, pragma_user_version').get();
|
|
85
|
+
requireFact(identity.application_id === APPLICATION_ID, 'unrelated SQLite database.');
|
|
86
|
+
requireFact(identity.user_version === CURRENT_USER_VERSION, 'unsupported SQLite format version; this build opens only fresh format-11 squares.');
|
|
87
|
+
validateSchema(db, [STORAGE_META_SQL, lower.schemaSql, AGORA_SCHEMA_SQL].join('\n'), location);
|
|
243
88
|
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
89
|
+
/** Explicit full-Square framing and reset, composed from the neutral store and the supplied Channel schema. */
|
|
90
|
+
export function squareStoreFormat(lower, resetStorage) {
|
|
91
|
+
return {
|
|
92
|
+
applicationId: APPLICATION_ID, userVersion: CURRENT_USER_VERSION, observerKey: 'square',
|
|
93
|
+
validate: (db, location) => validateSquareFraming(db, lower, location),
|
|
94
|
+
reset(db) { lower.reset(db); resetAgoraSqlite(db); resetStorage(db); },
|
|
95
|
+
};
|
|
251
96
|
}
|
|
252
97
|
const kinds = ['join', 'done', 'say', 'read', 'listen', 'ignore', 'unlisten'];
|
|
253
98
|
function head(row) {
|
|
@@ -258,10 +103,14 @@ function head(row) {
|
|
|
258
103
|
requireFact(row.reach === null || row.kind === 'say' && row.reach === 'bell');
|
|
259
104
|
if (row.reply_index !== null)
|
|
260
105
|
requireFact(row.kind === 'say' && integer(row.reply_index) < index);
|
|
106
|
+
const messageIndex = row.message_index === null || row.message_index === undefined ? undefined : integer(row.message_index);
|
|
107
|
+
requireFact((row.kind === 'say') === (messageIndex !== undefined));
|
|
261
108
|
return immutable({
|
|
262
|
-
index, at, kind: row.kind, actor, ...(row.reach === null ? {} : { reach: 'bell' }), ...(row.reply_index === null ? {} : { reply: row.reply_index })
|
|
109
|
+
index, at, kind: row.kind, actor, ...(row.reach === null ? {} : { reach: 'bell' }), ...(row.reply_index === null ? {} : { reply: row.reply_index }),
|
|
110
|
+
...(messageIndex === undefined ? {} : { messageIndex }),
|
|
263
111
|
});
|
|
264
112
|
}
|
|
113
|
+
/** A persisted upper activity. A say is a relation: it may not carry content and must name its message. */
|
|
265
114
|
export function validateStoredActivity(value) {
|
|
266
115
|
const a = object(value);
|
|
267
116
|
integer(a.index);
|
|
@@ -271,34 +120,15 @@ export function validateStoredActivity(value) {
|
|
|
271
120
|
string(a.actor, true);
|
|
272
121
|
switch (a.kind) {
|
|
273
122
|
case 'say':
|
|
274
|
-
shape(a, [...common], ['
|
|
275
|
-
|
|
276
|
-
requireFact(a.delivery !== undefined, 'say delivery is malformed.');
|
|
277
|
-
try {
|
|
278
|
-
validateActivityDelivery(a.delivery);
|
|
279
|
-
}
|
|
280
|
-
catch {
|
|
281
|
-
throw invalidArtifact('say delivery is malformed.');
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
if (Object.hasOwn(a, 'body'))
|
|
285
|
-
string(a.body);
|
|
286
|
-
if (Object.hasOwn(a, 'metadata')) {
|
|
287
|
-
requireFact(a.metadata !== undefined, 'say metadata is malformed.');
|
|
288
|
-
try {
|
|
289
|
-
validateActivityMetadata(a.metadata);
|
|
290
|
-
}
|
|
291
|
-
catch {
|
|
292
|
-
throw invalidArtifact('say metadata is malformed.');
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
requireFact((Object.hasOwn(a, 'body') && typeof a.body === 'string' && a.body.trim() !== '') || activityMetadataHasContent(a.metadata), 'say must contain a non-empty body or non-empty metadata.');
|
|
123
|
+
shape(a, [...common], ['mentions', 'reach', 'reply', 'messageIndex']);
|
|
124
|
+
requireFact(Object.hasOwn(a, 'messageIndex'), 'say is missing its message.');
|
|
296
125
|
if (Object.hasOwn(a, 'mentions'))
|
|
297
126
|
strings(a.mentions);
|
|
298
127
|
if (Object.hasOwn(a, 'reach'))
|
|
299
128
|
requireFact(a.reach === 'bell');
|
|
300
129
|
if (Object.hasOwn(a, 'reply'))
|
|
301
130
|
requireFact(integer(a.reply) < a.index);
|
|
131
|
+
integer(a.messageIndex);
|
|
302
132
|
break;
|
|
303
133
|
case 'done':
|
|
304
134
|
shape(a, common, ['body']);
|
|
@@ -319,13 +149,45 @@ export function validateStoredActivity(value) {
|
|
|
319
149
|
}
|
|
320
150
|
return value;
|
|
321
151
|
}
|
|
152
|
+
/** The explicit upper projection of a stored say onto its authoritative message content. */
|
|
153
|
+
export function validateHydratedActivity(value) {
|
|
154
|
+
const a = object(value);
|
|
155
|
+
if (a.kind !== 'say')
|
|
156
|
+
return validateStoredActivity(a);
|
|
157
|
+
const { body, metadata, ...relation } = a;
|
|
158
|
+
const stored = validateStoredActivity(relation);
|
|
159
|
+
if (body !== undefined)
|
|
160
|
+
string(body);
|
|
161
|
+
if (metadata !== undefined) {
|
|
162
|
+
try {
|
|
163
|
+
validateActivityMetadata(metadata);
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
throw invalidArtifact('say metadata is malformed.');
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
requireFact((typeof body === 'string' && body.trim() !== '') || activityMetadataHasContent(metadata), 'say must contain a non-empty body or non-empty metadata.');
|
|
170
|
+
return { ...stored, ...(body === undefined ? {} : { body: body }), ...(metadata === undefined ? {} : { metadata: metadata }) };
|
|
171
|
+
}
|
|
172
|
+
/** Stored Agora relation only: say content lives in its linked Channel message. */
|
|
322
173
|
function activity(row) {
|
|
323
174
|
const h = head(row), payload = object(json(row.payload_json));
|
|
324
|
-
// Payload cannot shadow indexed structural columns.
|
|
325
|
-
const allowed = h.kind === 'say' ? ['
|
|
175
|
+
// Payload cannot shadow indexed structural columns or message-owned content.
|
|
176
|
+
const allowed = h.kind === 'say' ? ['mentions'] : h.kind === 'done' ? ['body'] : h.kind === 'read' ? ['through'] : h.kind === 'listen' || h.kind === 'ignore' || h.kind === 'unlisten' ? ['target'] : [];
|
|
326
177
|
requireFact(Object.keys(payload).every(k => allowed.includes(k)));
|
|
327
178
|
return immutable(validateStoredActivity({ ...h, ...payload }));
|
|
328
179
|
}
|
|
180
|
+
/** Explicit upper projection: the stored say plus its authoritative Channel content. */
|
|
181
|
+
function activityWithContent(row) {
|
|
182
|
+
const stored = activity(row);
|
|
183
|
+
if (stored.kind !== 'say')
|
|
184
|
+
return stored;
|
|
185
|
+
requireFact(row.body_json !== null && row.body_json !== undefined, 'say message is missing.');
|
|
186
|
+
const metadata = row.metadata_json === null || row.metadata_json === undefined ? undefined : json(row.metadata_json);
|
|
187
|
+
const body = decoded(row.body_json);
|
|
188
|
+
return immutable(validateHydratedActivity({ ...stored,
|
|
189
|
+
...(body === '' ? {} : { body }), ...(metadata === undefined ? {} : { metadata }) }));
|
|
190
|
+
}
|
|
329
191
|
function participant(row) {
|
|
330
192
|
const name = decoded(row.name_json, true);
|
|
331
193
|
requireFact(row.name_key_json === key(name) && [0, 1].includes(row.joined) && [0, 1].includes(row.done) && !(row.joined && row.done));
|
|
@@ -357,135 +219,14 @@ export function validateWatchLease(value) {
|
|
|
357
219
|
}
|
|
358
220
|
return value;
|
|
359
221
|
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
return '{' + Object.keys(value).sort().map(k => encode(k) + ':' + stableJson(value[k])).join(',') + '}';
|
|
369
|
-
}
|
|
370
|
-
function activityId(value) {
|
|
371
|
-
const parsed = typeof value === 'string' ? /^act\/(0|[1-9]\d*)$/.exec(value) : null;
|
|
372
|
-
requireFact(parsed !== null, 'activity generation is malformed.');
|
|
373
|
-
const index = Number(parsed[1]);
|
|
374
|
-
requireFact(Number.isSafeInteger(index));
|
|
375
|
-
return value;
|
|
376
|
-
}
|
|
377
|
-
function activityIndex(value) {
|
|
378
|
-
const parsed = activityId(value);
|
|
379
|
-
return integer(Number(parsed.slice(4)));
|
|
380
|
-
}
|
|
381
|
-
function wakeAddress(value) {
|
|
382
|
-
const address = shape(value, ['kind', 'address']);
|
|
383
|
-
requireFact(isWakeRouteKind(address.kind));
|
|
384
|
-
const entries = object(address.address);
|
|
385
|
-
requireFact(Object.values(entries).every((item) => typeof item === 'string'));
|
|
386
|
-
stableJson(entries);
|
|
387
|
-
return immutable({ kind: address.kind, address: entries });
|
|
388
|
-
}
|
|
389
|
-
function ownerFrom(row) {
|
|
390
|
-
const owner = decoded(row.owner_json, true);
|
|
391
|
-
validateOwner(owner);
|
|
392
|
-
return owner;
|
|
393
|
-
}
|
|
394
|
-
function seat(row) {
|
|
395
|
-
const participant = decoded(row.participant_json, true);
|
|
396
|
-
requireFact(row.participant_key_json === key(participant));
|
|
397
|
-
const generation = activityId(decoded(row.generation_json, true));
|
|
398
|
-
const parsedWake = row.wake_address_json === null ? undefined : wakeAddress(json(row.wake_address_json));
|
|
399
|
-
requireFact(row.attachment === 'attached' || row.attachment === 'detached', 'seat attachment is malformed.');
|
|
400
|
-
requireFact(row.attachment !== 'detached' || parsedWake === undefined, 'a detached seat has a wake address.');
|
|
401
|
-
return immutable({ participant, owner: ownerFrom(row), generation, attachment: row.attachment, ...(parsedWake === undefined ? {} : { wakeAddress: parsedWake }) });
|
|
402
|
-
}
|
|
403
|
-
function nativeCorrelation(value) {
|
|
404
|
-
const item = shape(value, ['harness'], ['token', 'messageId', 'payloadHash']);
|
|
405
|
-
requireFact(['claude', 'opencode', 'pi'].includes(string(item.harness)));
|
|
406
|
-
for (const key of ['token', 'messageId', 'payloadHash'])
|
|
407
|
-
if (Object.hasOwn(item, key))
|
|
408
|
-
string(item[key], true);
|
|
409
|
-
return immutable({ harness: item.harness,
|
|
410
|
-
...(item.token === undefined ? {} : { token: item.token }),
|
|
411
|
-
...(item.messageId === undefined ? {} : { messageId: item.messageId }),
|
|
412
|
-
...(item.payloadHash === undefined ? {} : { payloadHash: item.payloadHash }) });
|
|
413
|
-
}
|
|
414
|
-
function deliveryEvidence(row, payload = object(json(row.payload_json))) {
|
|
415
|
-
const participant = decoded(row.participant_json, true);
|
|
416
|
-
requireFact(row.participant_key_json === key(participant));
|
|
417
|
-
const generation = activityId(decoded(row.generation_json, true));
|
|
418
|
-
const activity = activityId(`act/${integer(row.act_index)}`);
|
|
419
|
-
requireFact(['wake', 'presentation'].includes(string(row.kind)) && ['dispatching', 'accepted', 'failed', 'unknown', 'released', 'presented', 'clipped'].includes(string(row.outcome)));
|
|
420
|
-
requireFact(Number.isSafeInteger(row.attempt_n) && row.attempt_n >= 0);
|
|
421
|
-
const nativeObservations = payload.nativeObservations === undefined ? undefined : (() => {
|
|
422
|
-
requireFact(Array.isArray(payload.nativeObservations));
|
|
423
|
-
return payload.nativeObservations.map((value) => {
|
|
424
|
-
const item = shape(value, ['eventId', 'messageId', 'state', 'observedAt'], ['reason']);
|
|
425
|
-
requireFact(['held', 'released', 'dropped'].includes(string(item.state)));
|
|
426
|
-
return {
|
|
427
|
-
eventId: string(item.eventId, true), messageId: string(item.messageId, true),
|
|
428
|
-
state: item.state,
|
|
429
|
-
...(Object.hasOwn(item, 'reason') ? { reason: string(item.reason) } : {}), observedAt: finite(item.observedAt),
|
|
430
|
-
};
|
|
431
|
-
});
|
|
432
|
-
})();
|
|
433
|
-
return immutable({
|
|
434
|
-
participant, generation, activity, kind: row.kind,
|
|
435
|
-
outcome: row.outcome, attemptN: integer(row.attempt_n), at: finite(row.at),
|
|
436
|
-
...(row.expires_at === null ? {} : { expiresAt: finite(row.expires_at) }),
|
|
437
|
-
...(row.claim_token_json === null ? {} : { claimToken: decoded(row.claim_token_json, true) }),
|
|
438
|
-
...(payload.route === undefined ? {} : { route: wakeAddress(payload.route) }),
|
|
439
|
-
...(payload.signature === undefined ? {} : { signature: string(payload.signature) }),
|
|
440
|
-
...(payload.message === undefined ? {} : { message: string(payload.message) }),
|
|
441
|
-
...(payload.diagnostic === undefined ? {} : { diagnostic: payload.diagnostic }),
|
|
442
|
-
...(payload.correlation === undefined ? {} : { correlation: nativeCorrelation(payload.correlation) }),
|
|
443
|
-
...(nativeObservations === undefined ? {} : { nativeObservations }),
|
|
444
|
-
});
|
|
445
|
-
}
|
|
446
|
-
function deliveryDeferral(row) {
|
|
447
|
-
const participant = decoded(row.participant_json, true);
|
|
448
|
-
requireFact(row.participant_key_json === key(participant));
|
|
449
|
-
const generation = activityId(decoded(row.generation_json, true));
|
|
450
|
-
const id = validateDeferralId(decoded(row.deferral_id_json, true));
|
|
451
|
-
const match = validateMetadataMatch(json(row.match_json));
|
|
452
|
-
requireFact(encode(match) === row.match_json, 'delivery deferral match encoding is malformed.');
|
|
453
|
-
return immutable({ participant, generation, id, match, expiresAt: finite(row.expires_at) });
|
|
454
|
-
}
|
|
455
|
-
function normalizedSql(sql) { return sql.trim().replace(/;$/, '').replace(/\s+/g, ' ').replace(/\s*([(),=])\s*/g, '$1'); }
|
|
456
|
-
function validateOwnedSchema(db, expected, location) {
|
|
457
|
-
const actual = db.prepare("SELECT sql FROM sqlite_schema WHERE name NOT GLOB 'sqlite_*' AND sql IS NOT NULL").all().map(r => normalizedSql(string(r.sql))).sort();
|
|
458
|
-
requireFact(actual.length === expected.length && actual.every((sql, i) => sql === expected[i]), 'unsupported row schema.');
|
|
459
|
-
requireFact(db.prepare('PRAGMA journal_mode').get().journal_mode === (location === ':memory:' ? 'memory' : 'delete'), 'unsupported journal mode.');
|
|
460
|
-
}
|
|
461
|
-
export function validateRowFraming(db, version, location = '') {
|
|
462
|
-
if (version === V2_USER_VERSION || version === V3_USER_VERSION) {
|
|
463
|
-
const issue = legacyRowFramingIssue(db, APPLICATION_ID, version, location, FORMAT4_SCHEMA_SQL);
|
|
464
|
-
if (issue !== undefined)
|
|
465
|
-
throw invalidArtifact(issue);
|
|
466
|
-
return;
|
|
467
|
-
}
|
|
468
|
-
const identity = db.prepare('SELECT application_id,user_version FROM pragma_application_id,pragma_user_version').get();
|
|
469
|
-
requireFact(identity.application_id === APPLICATION_ID, 'unrelated SQLite database.');
|
|
470
|
-
requireFact(identity.user_version === version, 'unsupported SQLite format version.');
|
|
471
|
-
validateOwnedSchema(db, version === V4_USER_VERSION ? FORMAT4_SCHEMA : version === V5_USER_VERSION ? FORMAT5_SCHEMA : version === V6_USER_VERSION ? FORMAT6_SCHEMA : version === V7_USER_VERSION ? FORMAT7_SCHEMA : EXPECTED_SCHEMA_SQL, location);
|
|
472
|
-
}
|
|
473
|
-
export function validateSqliteHeader(db, location = '') {
|
|
474
|
-
const identity = db.prepare('SELECT application_id,user_version FROM pragma_application_id,pragma_user_version').get();
|
|
475
|
-
requireFact(identity.application_id === APPLICATION_ID, 'unrelated SQLite database.');
|
|
476
|
-
if (identity.user_version === 1) {
|
|
477
|
-
validateLegacyFraming(db, APPLICATION_ID, location);
|
|
478
|
-
throw new SquareError('invalid_args', `Square artifact at ${location} needs migration.\nStop sessions using it, back it up, then migrate and verify before restarting.`, { reason: 'migration_required' });
|
|
479
|
-
}
|
|
480
|
-
if (identity.user_version === V2_USER_VERSION || identity.user_version === V3_USER_VERSION || identity.user_version === V4_USER_VERSION || identity.user_version === V5_USER_VERSION || identity.user_version === V6_USER_VERSION || identity.user_version === V7_USER_VERSION) {
|
|
481
|
-
validateRowFraming(db, identity.user_version, location);
|
|
482
|
-
throw new SquareError('invalid_args', `Square artifact at ${location} needs migration.\nStop sessions using it, back it up, then migrate and verify before restarting.`, { reason: 'migration_required' });
|
|
483
|
-
}
|
|
484
|
-
if (identity.user_version === CURRENT_USER_VERSION) {
|
|
485
|
-
validateRowFraming(db, CURRENT_USER_VERSION, location);
|
|
486
|
-
return createSqliteDriver(db).header();
|
|
487
|
-
}
|
|
488
|
-
throw invalidArtifact('unsupported SQLite format version.');
|
|
222
|
+
export function readSquareHeader(db) {
|
|
223
|
+
const rows = db.prepare('SELECT m.*, (SELECT COALESCE(MAX(act_index),-1) FROM activities) AS last_act_index FROM square_meta m').all();
|
|
224
|
+
requireFact(rows.length === 1 && rows[0].id === 1, 'header row is malformed.');
|
|
225
|
+
const r = rows[0], nextActIndex = integer(r.next_act_index), lastActIndex = integer(r.last_act_index, -1);
|
|
226
|
+
requireFact(nextActIndex > lastActIndex, 'nextActIndex is behind the activity history.');
|
|
227
|
+
return immutable({ revision: readStorageRevision(db), deliveryRevision: readStorageRevision(db, true), nextActIndex, lastActIndex,
|
|
228
|
+
hardCap: r.hard_cap === null ? null : integer(r.hard_cap, 1), ...(r.throttle_per_minute === null ? {} : { throttlePerMinute: integer(r.throttle_per_minute, 1) }),
|
|
229
|
+
preamble: strings(json(r.preamble_json)), warmup: strings(json(r.warmup_json)) });
|
|
489
230
|
}
|
|
490
231
|
function activitySelection(filter, alias = 'a') {
|
|
491
232
|
const terms = [], args = [];
|
|
@@ -513,7 +254,8 @@ function activitySelection(filter, alias = 'a') {
|
|
|
513
254
|
terms.push(`NOT EXISTS (
|
|
514
255
|
SELECT 1 FROM json_each(?) field WHERE NOT EXISTS (
|
|
515
256
|
WITH RECURSIVE walk(value,type,depth) AS (
|
|
516
|
-
SELECT
|
|
257
|
+
SELECT CASE WHEN ${alias}.kind='say' THEN json_extract(m.metadata_json,'$') ELSE json_extract(${alias}.payload_json,'$.metadata') END,
|
|
258
|
+
CASE WHEN ${alias}.kind='say' THEN json_type(m.metadata_json,'$') ELSE json_type(${alias}.payload_json,'$.metadata') END,0
|
|
517
259
|
UNION ALL
|
|
518
260
|
SELECT child.value,child.type,walk.depth+1
|
|
519
261
|
FROM walk JOIN json_each(CASE WHEN walk.type='object' THEN walk.value ELSE '{}' END) child
|
|
@@ -529,134 +271,100 @@ function activitySelection(filter, alias = 'a') {
|
|
|
529
271
|
args.push(encode(filter.metadataMatch.fields));
|
|
530
272
|
}
|
|
531
273
|
if (filter.search !== undefined) {
|
|
532
|
-
terms.push(`square_search(
|
|
274
|
+
terms.push(`square_search(?,?,'act/'||${alias}.act_index,${alias}.actor_json,CASE WHEN ${alias}.kind='say' THEN json_extract(m.body_json,'$') ELSE json_extract(${alias}.payload_json,'$.body') END)=1`);
|
|
533
275
|
args.push(filter.search.mode, filter.search.text);
|
|
534
276
|
}
|
|
535
277
|
return { sql: terms.length ? terms.join(' AND ') : '1', args };
|
|
536
278
|
}
|
|
537
|
-
function
|
|
538
|
-
|
|
539
|
-
if (hasMore)
|
|
540
|
-
rows.pop();
|
|
541
|
-
if (page.edge === 'newest')
|
|
542
|
-
rows.reverse();
|
|
543
|
-
return immutable({ items: rows.map(decode), hasMore });
|
|
544
|
-
}
|
|
545
|
-
function pageLimit(page) {
|
|
546
|
-
requireFact(page.edge === 'oldest' || page.edge === 'newest');
|
|
547
|
-
return Math.min(integer(page.limit, 1) + 1, Number.MAX_SAFE_INTEGER);
|
|
548
|
-
}
|
|
549
|
-
export function createSqliteDriver(db) {
|
|
550
|
-
let changed = false, deliveryChanged = false;
|
|
279
|
+
export function createSqliteDriver(db, channelDriver = createChannelSqlite(db)) {
|
|
280
|
+
let changed = false;
|
|
551
281
|
const all = (sql, args = []) => db.prepare(sql).all(...args);
|
|
552
282
|
const one = (sql, args = []) => db.prepare(sql).get(...args);
|
|
553
283
|
const run = (sql, args = []) => Number(db.prepare(sql).run(...args).changes);
|
|
554
284
|
let searchRegistered = false;
|
|
285
|
+
function ensureSearch() {
|
|
286
|
+
if (searchRegistered)
|
|
287
|
+
return;
|
|
288
|
+
const patterns = new Map();
|
|
289
|
+
db.function('square_search', { deterministic: true }, (mode, text, index, actor, body) => {
|
|
290
|
+
const cacheKey = encode([mode, text]);
|
|
291
|
+
let pattern = patterns.get(cacheKey);
|
|
292
|
+
if (pattern === undefined) {
|
|
293
|
+
pattern = compileSearchPattern(string(text), mode === 'literal');
|
|
294
|
+
patterns.set(cacheKey, pattern);
|
|
295
|
+
}
|
|
296
|
+
return Number([string(index), decoded(actor, true), body === null ? '' : string(body)]
|
|
297
|
+
.some(field => pattern.test(field)));
|
|
298
|
+
});
|
|
299
|
+
searchRegistered = true;
|
|
300
|
+
}
|
|
555
301
|
function selectActivity(filter) {
|
|
556
|
-
if (filter.search !== undefined
|
|
557
|
-
|
|
558
|
-
db.function('square_search', { deterministic: true }, (mode, text, index, actor, body) => {
|
|
559
|
-
const key = encode([mode, text]);
|
|
560
|
-
let pattern = patterns.get(key);
|
|
561
|
-
if (pattern === undefined) {
|
|
562
|
-
pattern = compileSearchPattern(string(text), mode === 'literal');
|
|
563
|
-
patterns.set(key, pattern);
|
|
564
|
-
}
|
|
565
|
-
return Number([formatActivityId(integer(index)), decoded(actor, true), body === null ? '' : string(body)]
|
|
566
|
-
.some(field => pattern.test(field)));
|
|
567
|
-
});
|
|
568
|
-
searchRegistered = true;
|
|
569
|
-
}
|
|
302
|
+
if (filter.search !== undefined)
|
|
303
|
+
ensureSearch();
|
|
570
304
|
return activitySelection(filter);
|
|
571
305
|
}
|
|
306
|
+
const read = {};
|
|
307
|
+
const channel = channelDriver, channelRead = channel, channelWrite = channel;
|
|
308
|
+
const currentEndpoint = (name) => channel.endpoint(name);
|
|
572
309
|
const owner = (requested) => read.participant(requested)?.name ?? requested;
|
|
573
|
-
const attentionRows = (condition, args) => all(`SELECT t.*,p.name_json FROM attention t JOIN participants p ON p.name_key_json=t.listener_key_json WHERE ${condition} ORDER BY t.since_index`, args).map(attention);
|
|
574
310
|
function directedSelection(recipient, filter) {
|
|
575
|
-
const
|
|
311
|
+
const participant = read.participant(recipient), base = selectActivity({ ...filter, actors: filter.senders });
|
|
576
312
|
base.sql += ' AND d.recipient_key_json=?';
|
|
577
|
-
base.args.push(
|
|
313
|
+
base.args.push(participant ? key(participant.name) : '');
|
|
314
|
+
if (filter.currentGeneration) {
|
|
315
|
+
base.sql += ' AND (ma.generation_json IS NULL OR ma.generation_json=?)';
|
|
316
|
+
base.args.push(participant ? encode(currentEndpoint(participant.name)?.generation ?? '') : '');
|
|
317
|
+
}
|
|
578
318
|
if (filter.unseenOnly) {
|
|
579
|
-
base.sql += ' AND NOT EXISTS (SELECT 1 FROM receipts r WHERE r.
|
|
580
|
-
base.args.push(encode(
|
|
319
|
+
base.sql += ' AND NOT EXISTS (SELECT 1 FROM receipts r WHERE r.owner_key_json=? AND r.msg_index=a.message_index)';
|
|
320
|
+
base.args.push(encode(participant?.name ?? recipient));
|
|
581
321
|
}
|
|
582
322
|
if (filter.attentionOnly)
|
|
583
|
-
base.sql += " AND d.
|
|
584
|
-
return base;
|
|
585
|
-
}
|
|
586
|
-
function directedHead(row, recipient) {
|
|
587
|
-
const h = head(row);
|
|
588
|
-
requireFact(h.kind === 'say');
|
|
589
|
-
return immutable({
|
|
590
|
-
index: h.index, at: h.at, actor: h.actor, recipient, route: routeKind(row.route)
|
|
591
|
-
});
|
|
592
|
-
}
|
|
593
|
-
const directedFrom = 'landed_audience d JOIN activities a ON a.act_index=d.act_index';
|
|
594
|
-
const pendingFrom = `landed_audience d JOIN activities a ON a.act_index=d.act_index JOIN participants p ON p.name_key_json=d.recipient_key_json`;
|
|
595
|
-
const pendingWhere = `p.joined=1 AND d.act_index>COALESCE(p.last_join_index,-1) AND NOT EXISTS (SELECT 1 FROM receipts r WHERE r.owner_json=p.name_json AND r.act_index=d.act_index)`;
|
|
596
|
-
const latestSeats = `seats s JOIN participants p ON p.name_key_json=s.participant_key_json
|
|
597
|
-
JOIN activities a ON a.act_index=p.last_join_index AND a.kind='join' AND a.actor_key_json=p.name_key_json
|
|
598
|
-
AND s.generation_json='"act/'||a.act_index||'"'`;
|
|
599
|
-
function ownerSeats(owner) {
|
|
600
|
-
validateOwner(owner);
|
|
601
|
-
return immutable(all(`SELECT s.* FROM ${latestSeats} WHERE p.joined=1 AND s.owner_json=? ORDER BY s.participant_key_json`, [encode(owner)]).map(seat));
|
|
323
|
+
base.sql += " AND d.reason IN ('mention','reply','bell')";
|
|
324
|
+
return { base, participant };
|
|
602
325
|
}
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
const args = [key(participant), encode(generation)];
|
|
611
|
-
const filter = activityIndex === undefined ? '' : ' AND act_index=?';
|
|
612
|
-
if (activityIndex !== undefined)
|
|
613
|
-
args.push(integer(Number(activityIndex.slice(4))));
|
|
614
|
-
return immutable(all(`SELECT * FROM delivery_evidence WHERE participant_key_json=? AND generation_json=?${filter} ORDER BY act_index,kind,attempt_n`, args).map(row => deliveryEvidence(row)));
|
|
615
|
-
},
|
|
616
|
-
deliveryByToken(claimToken) {
|
|
617
|
-
const row = one('SELECT * FROM delivery_evidence WHERE claim_token_json=?', [encode(claimToken)]);
|
|
618
|
-
return row === undefined ? undefined : deliveryEvidence(row);
|
|
619
|
-
},
|
|
326
|
+
// ----- Upper Agora read capabilities -----
|
|
327
|
+
const lower = channelRead;
|
|
328
|
+
const readMethods = {
|
|
329
|
+
seat(name) { const endpointFact = currentEndpoint(name); return endpointFact === undefined ? undefined : endpointFact; },
|
|
330
|
+
seatsForOwner: (requested) => lower.endpointsForOwner(requested),
|
|
331
|
+
deliveryEvidence: (participant, generation, activity) => lower.deliveryEvidence(participant, generation, activity),
|
|
332
|
+
deliveryByToken: (token) => lower.deliveryByToken(token),
|
|
620
333
|
cancelledThrough(participant, generation) {
|
|
621
|
-
const
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
});
|
|
632
|
-
},
|
|
633
|
-
participant(name) {
|
|
634
|
-
const r = one('SELECT * FROM participants WHERE name_key_json=?', [key(name)]);
|
|
635
|
-
return r ? participant(r) : undefined;
|
|
636
|
-
},
|
|
334
|
+
const boundary = lower.cancelledThrough(participant, generation);
|
|
335
|
+
if (boundary === undefined)
|
|
336
|
+
return undefined;
|
|
337
|
+
if (boundary < 0)
|
|
338
|
+
return -1;
|
|
339
|
+
const row = one('SELECT MAX(act_index) AS act_index FROM activities WHERE message_index IS NOT NULL AND message_index<=?', [boundary]);
|
|
340
|
+
return row?.act_index === null || row?.act_index === undefined ? -1 : integer(row.act_index);
|
|
341
|
+
},
|
|
342
|
+
header() { return readSquareHeader(db); },
|
|
343
|
+
participant(name) { const r = one('SELECT * FROM participants WHERE name_key_json=?', [key(name)]); return r ? participant(r) : undefined; },
|
|
637
344
|
participantCounts() {
|
|
638
345
|
const r = one('SELECT COUNT(*) AS known, COALESCE(SUM(joined),0) AS joined, COALESCE(SUM(done),0) AS done FROM participants');
|
|
639
346
|
return immutable({ known: integer(r.known), joined: integer(r.joined), done: integer(r.done) });
|
|
640
347
|
},
|
|
641
348
|
roster: () => immutable(all('SELECT * FROM participants ORDER BY first_index').map(participant)),
|
|
642
|
-
attention: name => immutable(
|
|
643
|
-
incomingAttention: name => immutable(
|
|
349
|
+
attention: name => immutable(all('SELECT t.*,p.name_json FROM attention t JOIN participants p ON p.name_key_json=t.listener_key_json WHERE t.listener_key_json=? ORDER BY t.since_index', [key(name)]).map(attention)),
|
|
350
|
+
incomingAttention: name => immutable(all('SELECT t.*,p.name_json FROM attention t JOIN participants p ON p.name_key_json=t.listener_key_json WHERE t.target_key_json=? ORDER BY t.since_index', [key(name)]).map(attention)),
|
|
644
351
|
activity(index) {
|
|
645
352
|
const r = one('SELECT * FROM activities WHERE act_index=?', [integer(index)]);
|
|
646
353
|
return r ? activity(r) : undefined;
|
|
647
354
|
},
|
|
355
|
+
activityByMessage(index) {
|
|
356
|
+
const r = one("SELECT * FROM activities WHERE message_index=? AND kind='say'", [integer(index)]);
|
|
357
|
+
return r ? activity(r) : undefined;
|
|
358
|
+
},
|
|
648
359
|
activityHeads(filter, page) {
|
|
649
360
|
const s = selectActivity(filter), limit = pageLimit(page);
|
|
650
|
-
return pageRows(all(`SELECT a.
|
|
361
|
+
return pageRows(all(`SELECT a.*,m.body_json,m.metadata_json FROM activities a LEFT JOIN messages m ON m.msg_index=a.message_index WHERE ${s.sql} ORDER BY a.act_index ${page.edge === 'newest' ? 'DESC' : 'ASC'} LIMIT ?`, [...s.args, limit]), page, head);
|
|
651
362
|
},
|
|
652
363
|
activities(indexes) {
|
|
653
364
|
indexes.forEach(v => integer(v));
|
|
654
|
-
return immutable(all('SELECT
|
|
655
|
-
},
|
|
656
|
-
activityCount(filter) {
|
|
657
|
-
const s = selectActivity(filter);
|
|
658
|
-
return integer(one(`SELECT COUNT(*) AS n FROM activities a WHERE ${s.sql}`, s.args).n);
|
|
365
|
+
return immutable(all('SELECT a.*,m.body_json,m.metadata_json FROM activities a LEFT JOIN messages m ON m.msg_index=a.message_index WHERE a.act_index IN (SELECT value FROM json_each(?)) ORDER BY a.act_index', [encode(indexes)]).map(activityWithContent));
|
|
659
366
|
},
|
|
367
|
+
activityCount(filter) { const s = selectActivity(filter); return integer(one(`SELECT COUNT(*) AS n FROM activities a LEFT JOIN messages m ON m.msg_index=a.message_index WHERE ${s.sql}`, s.args).n); },
|
|
660
368
|
contextIndexes(centers, before, after) {
|
|
661
369
|
integer(before);
|
|
662
370
|
integer(after);
|
|
@@ -676,7 +384,6 @@ export function createSqliteDriver(db) {
|
|
|
676
384
|
sayOrdinals(indexes) {
|
|
677
385
|
indexes.forEach(v => integer(v));
|
|
678
386
|
const result = {};
|
|
679
|
-
// Only an actor's latest say can use its authoritative lifetime count.
|
|
680
387
|
for (const r of all(`SELECT a.act_index, CASE
|
|
681
388
|
WHEN p.say_count IS NOT NULL AND a.act_index=(SELECT MAX(s.act_index) FROM activities s WHERE s.kind='say' AND s.actor_key_json=a.actor_key_json)
|
|
682
389
|
THEN p.say_count
|
|
@@ -699,131 +406,63 @@ export function createSqliteDriver(db) {
|
|
|
699
406
|
args.push(integer(take));
|
|
700
407
|
return immutable(all(`SELECT at FROM activities WHERE ${where} ORDER BY at DESC,act_index DESC LIMIT ?`, args).map(r => finite(r.at)).reverse());
|
|
701
408
|
},
|
|
702
|
-
audience(index) { return immutable(all('SELECT p.name_json,d.
|
|
409
|
+
audience(index) { return immutable(all('SELECT p.name_json,d.reason,ma.generation_json FROM landed_audience d JOIN participants p ON p.name_key_json=d.recipient_key_json JOIN activities a ON a.act_index=d.act_index JOIN message_addresses ma ON ma.msg_index=a.message_index AND ma.recipient_key_json=d.recipient_key_json WHERE d.act_index=? ORDER BY d.recipient_order', [integer(index)]).map(r => ({ recipient: decoded(r.name_json, true), route: routeKind(r.reason), ...(r.generation_json === null ? {} : { generation: endpointGeneration(decoded(r.generation_json, true)) }) }))); },
|
|
703
410
|
directed(recipient, filter, page) {
|
|
704
|
-
const
|
|
705
|
-
|
|
411
|
+
const { base, participant: p } = directedSelection(recipient, filter), limit = pageLimit(page);
|
|
412
|
+
const rows = all(`SELECT a.*,m.body_json,m.metadata_json,d.reason,ma.generation_json FROM activities a JOIN messages m ON m.msg_index=a.message_index JOIN landed_audience d ON d.act_index=a.act_index JOIN message_addresses ma ON ma.msg_index=a.message_index AND ma.recipient_key_json=d.recipient_key_json WHERE ${base.sql} ORDER BY a.act_index ${page.edge === 'newest' ? 'DESC' : 'ASC'} LIMIT ?`, [...base.args, limit]);
|
|
413
|
+
return pageRows(rows, page, r => {
|
|
414
|
+
const h = head(r);
|
|
415
|
+
return { index: h.index, at: h.at, actor: h.actor, recipient: p?.name ?? recipient,
|
|
416
|
+
route: routeKind(r.reason), messageIndex: h.messageIndex,
|
|
417
|
+
...(r.generation_json === null ? {} : { generation: endpointGeneration(decoded(r.generation_json, true)) }) };
|
|
418
|
+
});
|
|
706
419
|
},
|
|
707
420
|
directedAt(recipient, index) { return read.directed(recipient, { afterIndex: integer(index) - 1, throughIndex: index }, { edge: 'oldest', limit: 1 }).items[0]; },
|
|
708
421
|
directedCount(recipient, filter) {
|
|
709
|
-
const
|
|
710
|
-
return integer(one(`SELECT COUNT(*) AS n FROM
|
|
711
|
-
},
|
|
712
|
-
pendingAudience(after, through, limit) {
|
|
713
|
-
const args = [integer(after, -1), integer(through, -1)], extra = pageLimit({ edge: 'oldest', limit });
|
|
714
|
-
const indices = all(`SELECT DISTINCT d.act_index FROM ${pendingFrom} WHERE ${pendingWhere} AND d.act_index>? AND d.act_index<=? ORDER BY d.act_index LIMIT ?`, [...args, extra]);
|
|
715
|
-
const hasMore = indices.length > limit;
|
|
716
|
-
if (hasMore)
|
|
717
|
-
indices.pop();
|
|
718
|
-
const groups = indices.map(r => {
|
|
719
|
-
const rows = all(`SELECT a.act_index,a.at,a.kind,a.actor_json,a.actor_key_json,a.reach,a.reply_index,p.name_json,d.route FROM ${pendingFrom} WHERE ${pendingWhere} AND d.act_index=? ORDER BY d.recipient_order`, [integer(r.act_index)]);
|
|
720
|
-
const h = head(rows[0]);
|
|
721
|
-
requireFact(h.kind === 'say');
|
|
722
|
-
return {
|
|
723
|
-
index: h.index, at: h.at, actor: h.actor, recipients: rows.map(v => ({ recipient: decoded(v.name_json, true), route: routeKind(v.route) }))
|
|
724
|
-
};
|
|
725
|
-
});
|
|
726
|
-
return immutable({ groups, hasMore });
|
|
422
|
+
const { base } = directedSelection(recipient, filter);
|
|
423
|
+
return integer(one(`SELECT COUNT(*) AS n FROM activities a LEFT JOIN messages m ON m.msg_index=a.message_index JOIN landed_audience d ON d.act_index=a.act_index JOIN message_addresses ma ON ma.msg_index=a.message_index AND ma.recipient_key_json=d.recipient_key_json WHERE ${base.sql}`, base.args).n);
|
|
727
424
|
},
|
|
728
425
|
receipt(name, index) {
|
|
729
|
-
const r = one('SELECT at FROM receipts WHERE
|
|
426
|
+
const r = one('SELECT r.at FROM receipts r JOIN activities a ON a.message_index=r.msg_index WHERE r.owner_key_json=? AND a.act_index=?', [encode(owner(name)), integer(index)]);
|
|
730
427
|
return r ? immutable({ at: finite(r.at) }) : undefined;
|
|
731
428
|
},
|
|
732
429
|
receiptFacts(name, afterIndex) {
|
|
733
|
-
const k = encode(owner(name))
|
|
430
|
+
const k = encode(owner(name));
|
|
431
|
+
const r = one('SELECT MAX(at) AS latest FROM receipts WHERE owner_key_json=?', [k]);
|
|
432
|
+
const e = one('SELECT MAX(a.act_index) AS latest FROM receipts r JOIN activities a ON a.message_index=r.msg_index WHERE r.owner_key_json=? AND a.act_index>?', [k, integer(afterIndex, -1)]);
|
|
734
433
|
return immutable({ ...(r.latest === null ? {} : { latestAt: finite(r.latest) }), ...(e.latest === null ? {} : { latestActorActivityIndex: integer(e.latest) }) });
|
|
735
434
|
},
|
|
736
|
-
deliveryDeferrals(name, generation) {
|
|
737
|
-
const canonical = owner(name);
|
|
738
|
-
const current = read.seat(canonical);
|
|
739
|
-
if (current === undefined || current.generation !== generation)
|
|
740
|
-
return [];
|
|
741
|
-
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));
|
|
742
|
-
},
|
|
743
435
|
lease(name) {
|
|
744
|
-
const
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
436
|
+
const lease = lower.claimLease(name, 'agora');
|
|
437
|
+
if (lease === undefined)
|
|
438
|
+
return undefined;
|
|
439
|
+
const selector = object(lease.selector);
|
|
440
|
+
const parsed = { leaseId: lease.id, heartbeatAt: lease.heartbeatAt, expiresAt: lease.expiresAt };
|
|
441
|
+
if (selector.filter !== undefined)
|
|
442
|
+
parsed.filter = selector.filter;
|
|
443
|
+
return immutable(validateWatchLease(parsed));
|
|
444
|
+
},
|
|
445
|
+
deliveryDeferrals: (participant, generation) => lower.deliveryDeferrals(participant, generation),
|
|
752
446
|
};
|
|
447
|
+
Object.assign(read, readMethods, { channel: channelRead });
|
|
448
|
+
// ----- Upper Agora writes / landing -----
|
|
753
449
|
const writes = {
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
return false;
|
|
761
|
-
const wake = value.wakeAddress === undefined ? null : encode(value.wakeAddress);
|
|
762
|
-
run('INSERT INTO seats(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), wake, value.attachment]);
|
|
763
|
-
run('DELETE FROM catch_leases WHERE owner_json=?', [encode(value.participant)]);
|
|
764
|
-
run('DELETE FROM delivery_deferrals WHERE participant_key_json=?', [key(value.participant)]);
|
|
765
|
-
deliveryChanged = true;
|
|
766
|
-
return true;
|
|
767
|
-
},
|
|
768
|
-
updateWakeAddress(value, wakeAddress) {
|
|
769
|
-
const current = one('SELECT wake_address_json FROM seats WHERE participant_key_json=? AND generation_json=?', [key(value.participant), encode(value.generation)]);
|
|
770
|
-
if (wakeAddress !== undefined) {
|
|
771
|
-
requireFact(isWakeRouteKind(wakeAddress.kind));
|
|
772
|
-
requireFact(Object.values(wakeAddress.address).every((item) => typeof item === 'string'));
|
|
773
|
-
}
|
|
774
|
-
if (stableJson(current.wake_address_json === null ? null : json(current.wake_address_json)) === stableJson(wakeAddress ?? null))
|
|
775
|
-
return false;
|
|
776
|
-
const count = run('UPDATE seats SET wake_address_json=? WHERE participant_key_json=? AND generation_json=?', [wakeAddress === undefined ? null : encode(wakeAddress), key(value.participant), encode(value.generation)]);
|
|
777
|
-
if (count)
|
|
778
|
-
deliveryChanged = true;
|
|
779
|
-
return count > 0;
|
|
780
|
-
},
|
|
781
|
-
reattachSeat(value, address) {
|
|
782
|
-
const current = read.seat(value.participant);
|
|
783
|
-
if (current?.attachment !== 'detached')
|
|
784
|
-
return false;
|
|
785
|
-
run("UPDATE seats 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)]);
|
|
786
|
-
deliveryChanged = true;
|
|
787
|
-
return true;
|
|
788
|
-
},
|
|
789
|
-
detachSeat(value) {
|
|
790
|
-
const count = run("UPDATE seats SET attachment='detached',wake_address_json=NULL WHERE participant_key_json=? AND generation_json=? AND attachment='attached'", [key(value.participant), encode(value.generation)]);
|
|
791
|
-
if (!count)
|
|
792
|
-
return false;
|
|
793
|
-
run('DELETE FROM catch_leases WHERE owner_json=?', [encode(value.participant)]);
|
|
794
|
-
run('DELETE FROM delivery_deferrals WHERE participant_key_json=? AND generation_json=?', [key(value.participant), encode(value.generation)]);
|
|
795
|
-
deliveryChanged = true;
|
|
796
|
-
return true;
|
|
797
|
-
},
|
|
798
|
-
removeSeat(value) {
|
|
799
|
-
const row = one(`SELECT s.* FROM ${latestSeats} WHERE p.done=1 AND s.participant_key_json=?`, [key(value.participant)]);
|
|
800
|
-
if (row === undefined || decoded(row.generation_json) !== value.generation || ownerFrom(row) !== value.owner)
|
|
801
|
-
return false;
|
|
802
|
-
run('DELETE FROM seats WHERE participant_key_json=?', [key(value.participant)]);
|
|
803
|
-
run('DELETE FROM catch_leases WHERE owner_json=?', [encode(value.participant)]);
|
|
804
|
-
run('DELETE FROM delivery_deferrals WHERE participant_key_json=?', [key(value.participant)]);
|
|
805
|
-
changed = true;
|
|
806
|
-
deliveryChanged = true;
|
|
807
|
-
return true;
|
|
808
|
-
},
|
|
809
|
-
cancelThrough(value, through) {
|
|
810
|
-
activityId(value.generation);
|
|
811
|
-
integer(through);
|
|
812
|
-
const prior = read.cancelledThrough(value.participant, value.generation) ?? -1;
|
|
813
|
-
if (through <= prior)
|
|
814
|
-
return false;
|
|
815
|
-
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]);
|
|
816
|
-
deliveryChanged = true;
|
|
817
|
-
return true;
|
|
818
|
-
},
|
|
450
|
+
channel: channelWrite,
|
|
451
|
+
storeSeat(value) { return channelWrite.storeEndpoint(value); },
|
|
452
|
+
updateWakeAddress(value, wakeAddress) { return channelWrite.updateWakeAddress(value, wakeAddress); },
|
|
453
|
+
reattachSeat(value, address) { return channelWrite.reattachEndpoint(value, address); },
|
|
454
|
+
detachSeat(value) { return channelWrite.detachEndpoint(value); },
|
|
455
|
+
removeSeat(value) { return channelWrite.removeEndpoint(value); },
|
|
819
456
|
applyLanding(effect) {
|
|
820
457
|
const a = validateStoredActivity(effect.activity);
|
|
821
458
|
requireFact(a.index >= read.header().nextActIndex && a.index < Number.MAX_SAFE_INTEGER, 'activity index cannot advance.');
|
|
822
|
-
const { index, at, kind, actor, reach, reply, ...payload } = a;
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
459
|
+
const { index, at, kind, actor, reach, reply, messageIndex, ...payload } = a;
|
|
460
|
+
const storedPayload = payload;
|
|
461
|
+
run('INSERT INTO activities(act_index,at,kind,actor_json,actor_key_json,reach,reply_index,message_index,payload_json) VALUES(?,?,?,?,?,?,?,?,?)', [index, at, kind, encode(actor), key(actor), reach ?? null, reply ?? null, messageIndex ?? null, encode(storedPayload)]);
|
|
462
|
+
// One message address per landed recipient is written by land() through publishMessage.
|
|
463
|
+
effect.audience.forEach((address, order) => {
|
|
464
|
+
run('INSERT INTO landed_audience(act_index,recipient_key_json,recipient_order,reason) VALUES(?,?,?,?)', [index, key(address.recipient), order, address.route]);
|
|
465
|
+
});
|
|
827
466
|
if (effect.participant) {
|
|
828
467
|
const p = effect.participant;
|
|
829
468
|
string(p.name, true);
|
|
@@ -853,199 +492,77 @@ export function createSqliteDriver(db) {
|
|
|
853
492
|
run('INSERT INTO attention(listener_key_json,target_key_json,target_json,kind,since_index) VALUES(?,?,?,?,?) ON CONFLICT(listener_key_json,target_key_json) DO UPDATE SET target_json=excluded.target_json,kind=excluded.kind,since_index=excluded.since_index', [key(t.listener), key(t.target), encode(t.target), t.kind, t.sinceIndex]);
|
|
854
493
|
}
|
|
855
494
|
run('UPDATE square_meta SET next_act_index=? WHERE id=1', [index + 1]);
|
|
495
|
+
extendLandingCatchCoverage(read, channelWrite.putReceiveClaim, a, effect.audience);
|
|
856
496
|
changed = true;
|
|
857
497
|
},
|
|
858
|
-
|
|
859
|
-
finite(at);
|
|
860
|
-
const next = read.header().nextActIndex;
|
|
861
|
-
indexes.forEach(index => requireFact(integer(index) < next, 'receipt references an unassigned activity index.'));
|
|
862
|
-
const canonical = owner(name);
|
|
863
|
-
string(canonical, true);
|
|
864
|
-
const count = recordConsumed({
|
|
865
|
-
insertReceipt: (ref, time) => run('INSERT OR IGNORE INTO receipts(owner_json,act_index,at) VALUES(?,?,?)', [encode(ref.recipient), ref.item, time]) > 0,
|
|
866
|
-
}, indexes.map(item => ({ recipient: canonical, item })), at);
|
|
867
|
-
if (count)
|
|
868
|
-
changed = true;
|
|
869
|
-
return count;
|
|
870
|
-
},
|
|
871
|
-
putLease(name, value, generation) {
|
|
498
|
+
putLease(name, value, generation, suppressed) {
|
|
872
499
|
const l = validateWatchLease(value), canonical = owner(name);
|
|
873
|
-
const old = read.lease(canonical), row = one('SELECT generation_json FROM catch_leases WHERE owner_json=?', [encode(canonical)]);
|
|
874
500
|
if (generation !== undefined)
|
|
875
|
-
|
|
876
|
-
|
|
501
|
+
endpointGeneration(generation);
|
|
502
|
+
const existing = channelRead.claimLease(canonical, 'agora');
|
|
503
|
+
if (existing !== undefined && generation !== undefined && existing.generation !== generation)
|
|
877
504
|
return false;
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
changed = true;
|
|
883
|
-
return true;
|
|
884
|
-
},
|
|
885
|
-
putDeliveryDeferral(seatRef, id, match, expiresAt, now) {
|
|
886
|
-
finite(now);
|
|
887
|
-
finite(expiresAt);
|
|
888
|
-
const canonicalMatch = validateMetadataMatch(match);
|
|
889
|
-
run('DELETE FROM delivery_deferrals WHERE participant_key_json=? AND expires_at<=?', [key(seatRef.participant), now]);
|
|
890
|
-
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]);
|
|
891
|
-
deliveryChanged = true;
|
|
892
|
-
return true;
|
|
893
|
-
},
|
|
894
|
-
renewDeliveryDeferral(seatRef, id, expiresAt, now) {
|
|
895
|
-
finite(now);
|
|
896
|
-
finite(expiresAt);
|
|
897
|
-
const pruned = run('DELETE FROM delivery_deferrals WHERE participant_key_json=? AND expires_at<=?', [key(seatRef.participant), now]);
|
|
898
|
-
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]);
|
|
899
|
-
if (count || pruned)
|
|
900
|
-
deliveryChanged = true;
|
|
901
|
-
return count > 0;
|
|
902
|
-
},
|
|
903
|
-
releaseDeliveryDeferral(seatRef, id, now) {
|
|
904
|
-
finite(now);
|
|
905
|
-
const pruned = run('DELETE FROM delivery_deferrals WHERE participant_key_json=? AND expires_at<=?', [key(seatRef.participant), now]);
|
|
906
|
-
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]);
|
|
907
|
-
if (count || pruned)
|
|
908
|
-
deliveryChanged = true;
|
|
909
|
-
return count > 0;
|
|
505
|
+
return channelWrite.putReceiveClaim({ namespace: 'agora', id: l.leaseId, owner: canonical,
|
|
506
|
+
generation: generation ?? existing?.generation ?? currentEndpoint(canonical)?.generation ?? 'detached',
|
|
507
|
+
selector: { ...(l.filter === undefined ? {} : { filter: l.filter }) },
|
|
508
|
+
suppressed: suppressed ?? [], heartbeatAt: l.heartbeatAt, expiresAt: l.expiresAt });
|
|
910
509
|
},
|
|
911
510
|
removeLease(name, expected, generation) {
|
|
912
|
-
const canonical = owner(name),
|
|
913
|
-
if (
|
|
511
|
+
const canonical = owner(name), existing = channelRead.claimLease(canonical, 'agora');
|
|
512
|
+
if (existing === undefined || existing.id !== expected || generation !== undefined && existing.generation !== generation)
|
|
914
513
|
return false;
|
|
915
|
-
|
|
916
|
-
if (count)
|
|
917
|
-
changed = true;
|
|
918
|
-
return count > 0;
|
|
514
|
+
return channelWrite.removeReceiveClaim(canonical, 'agora', expected);
|
|
919
515
|
},
|
|
920
516
|
};
|
|
921
|
-
function
|
|
922
|
-
const
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
}
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
}
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
deliveryChanged = true;
|
|
947
|
-
},
|
|
948
|
-
replace(value) {
|
|
949
|
-
const count = run('UPDATE delivery_evidence SET outcome=?,at=?,expires_at=?,payload_json=? WHERE participant_key_json=? AND generation_json=? AND act_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)]);
|
|
950
|
-
if (count)
|
|
951
|
-
deliveryChanged = true;
|
|
952
|
-
},
|
|
953
|
-
};
|
|
517
|
+
function importLanding(activityValue) {
|
|
518
|
+
const validated = validateHydratedActivity(activityValue);
|
|
519
|
+
if (validated.kind !== 'say') {
|
|
520
|
+
const effect = deriveLanding(validated, landingFacts(read, validated));
|
|
521
|
+
driver.applyLanding(effect);
|
|
522
|
+
// Imported custody: a join establishes the endpoint its activity coordinate names; a done drops it.
|
|
523
|
+
if (validated.kind === 'join')
|
|
524
|
+
channelWrite.storeEndpoint({ participant: validated.actor, owner: 'import', generation: formatActivityId(validated.index), attachment: 'attached' });
|
|
525
|
+
else if (validated.kind === 'done') {
|
|
526
|
+
const current = channelRead.endpoint(validated.actor);
|
|
527
|
+
if (current !== undefined)
|
|
528
|
+
channelWrite.removeEndpoint(current);
|
|
529
|
+
}
|
|
530
|
+
return validated;
|
|
531
|
+
}
|
|
532
|
+
const effect = deriveLanding(validated, landingFacts(read, validated));
|
|
533
|
+
const published = channelWrite.publishMessage({ from: validated.actor, at: validated.at,
|
|
534
|
+
body: validated.body ?? '', ...(validated.metadata === undefined ? {} : { metadata: validated.metadata }),
|
|
535
|
+
to: effect.audience.map(({ recipient, generation }) => ({ recipient, ...(generation === undefined ? {} : { generation }) })) });
|
|
536
|
+
const landed = { kind: 'say', index: validated.index, at: validated.at, actor: validated.actor, messageIndex: published.index,
|
|
537
|
+
...(validated.mentions === undefined ? {} : { mentions: validated.mentions }),
|
|
538
|
+
...(validated.reach === undefined ? {} : { reach: validated.reach }),
|
|
539
|
+
...(validated.reply === undefined ? {} : { reply: validated.reply }) };
|
|
540
|
+
driver.applyLanding({ ...effect, activity: landed });
|
|
541
|
+
return landed;
|
|
954
542
|
}
|
|
955
|
-
const
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
const index = activityIndex(input.activity);
|
|
959
|
-
finite(input.at);
|
|
960
|
-
const leaseMs = integer(input.leaseMs, 1);
|
|
961
|
-
string(input.claimToken, true);
|
|
962
|
-
if (input.route !== undefined)
|
|
963
|
-
wakeAddress(input.route);
|
|
964
|
-
const currentSeat = read.seat(input.seat.participant);
|
|
965
|
-
const item = read.activity(index);
|
|
966
|
-
if (currentSeat === undefined || currentSeat.attachment !== 'attached' || currentSeat.generation !== input.seat.generation || currentSeat.owner !== input.seat.owner
|
|
967
|
-
|| item?.kind !== 'say' || read.directedAt(input.seat.participant, index) === undefined)
|
|
968
|
-
return { status: 'stale' };
|
|
969
|
-
if (matchingDeferrals(read.deliveryDeferrals(currentSeat.participant, currentSeat.generation), item.metadata, input.at).length > 0)
|
|
970
|
-
return { status: 'deferred' };
|
|
971
|
-
const cancelledThrough = read.cancelledThrough(input.seat.participant, input.seat.generation);
|
|
972
|
-
if (cancelledThrough !== undefined && index <= cancelledThrough)
|
|
973
|
-
return { status: 'stale' };
|
|
974
|
-
const rows = attemptRows(input.seat, index, input.kind);
|
|
975
|
-
const result = claimAttempt({ ...rows, replace(value) {
|
|
976
|
-
rows.replace({ ...value, data: { ...value.data, diagnostic: 'interrupted dispatch' } });
|
|
977
|
-
} }, { purpose: input.kind === 'wake' ? 'dispatch' : 'presentation', token: input.claimToken,
|
|
978
|
-
at: input.at, leaseMs, data: input.route === undefined ? {} : { route: input.route } });
|
|
979
|
-
if (result.status === 'acquired')
|
|
980
|
-
return { status: 'acquired', claimToken: result.token, attemptN: result.ordinal };
|
|
981
|
-
const evidence = result.interrupted ? { ...result.evidence, data: { ...result.evidence.data, diagnostic: 'interrupted dispatch' } } : result.evidence;
|
|
982
|
-
return { status: result.status, evidence: rows.evidence(evidence) };
|
|
983
|
-
},
|
|
984
|
-
prepareDelivery(input) {
|
|
985
|
-
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)]);
|
|
986
|
-
if (row === undefined || row.outcome !== 'dispatching')
|
|
987
|
-
return false;
|
|
988
|
-
const previous = attempt(row), correlation = nativeCorrelation(input.correlation), payload = previous.data;
|
|
989
|
-
if (stableJson(payload.correlation ?? null) === stableJson(correlation))
|
|
990
|
-
return true;
|
|
991
|
-
return prepareAttempt(attemptRows(input.seat, integer(row.act_index), row.kind, previous), { token: input.claimToken, data: { ...payload, correlation } });
|
|
992
|
-
},
|
|
993
|
-
completeDelivery(input) {
|
|
994
|
-
const index = activityIndex(input.activity);
|
|
995
|
-
finite(input.at);
|
|
996
|
-
requireFact(['accepted', 'failed', 'unknown', 'released', 'presented', 'clipped'].includes(input.outcome));
|
|
997
|
-
string(input.claimToken, true);
|
|
998
|
-
const row = one('SELECT * FROM delivery_evidence WHERE participant_key_json=? AND generation_json=? AND act_index=? AND claim_token_json=?', [key(input.seat.participant), encode(input.seat.generation), index, encode(input.claimToken)]);
|
|
999
|
-
if (row === undefined)
|
|
1000
|
-
return false;
|
|
1001
|
-
const previous = attempt(row);
|
|
1002
|
-
const payload = { ...previous.data, ...(input.signature === undefined ? {} : { signature: input.signature }), ...(input.message === undefined ? {} : { message: input.message }), ...(input.diagnostic === undefined ? {} : { diagnostic: input.diagnostic }), ...(input.correlation === undefined ? {} : { correlation: input.correlation }) };
|
|
1003
|
-
return settleAttempt(attemptRows(input.seat, index, row.kind, previous), { token: input.claimToken, outcome: input.outcome, at: input.at, data: payload });
|
|
1004
|
-
},
|
|
1005
|
-
observeNativeDelivery(input) {
|
|
1006
|
-
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)]);
|
|
1007
|
-
if (row === undefined || !['dispatching', 'accepted', 'unknown'].includes(string(row.outcome)))
|
|
1008
|
-
return false;
|
|
1009
|
-
const observation = input.observation;
|
|
1010
|
-
requireFact(typeof observation.eventId === 'string' && observation.eventId.length > 0);
|
|
1011
|
-
requireFact(typeof observation.messageId === 'string' && observation.messageId.length > 0);
|
|
1012
|
-
requireFact(observation.state === 'held' || observation.state === 'released' || observation.state === 'dropped');
|
|
1013
|
-
finite(observation.observedAt);
|
|
1014
|
-
if (observation.reason !== undefined)
|
|
1015
|
-
string(observation.reason);
|
|
1016
|
-
const payload = object(json(row.payload_json));
|
|
1017
|
-
const observations = payload.nativeObservations === undefined ? [] : payload.nativeObservations;
|
|
1018
|
-
requireFact(Array.isArray(observations));
|
|
1019
|
-
if (observations.some((item) => object(item).eventId === observation.eventId))
|
|
1020
|
-
return true;
|
|
1021
|
-
const next = [...observations, observation];
|
|
1022
|
-
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)]);
|
|
1023
|
-
if (count)
|
|
1024
|
-
deliveryChanged = true;
|
|
1025
|
-
return count > 0;
|
|
1026
|
-
},
|
|
1027
|
-
};
|
|
1028
|
-
return Object.defineProperties(Object.assign(read, writes, delivery), {
|
|
1029
|
-
changed: { get: () => changed },
|
|
1030
|
-
deliveryChanged: { get: () => deliveryChanged },
|
|
543
|
+
const driver = Object.defineProperties(Object.assign(read, writes, { channel, importLanding }), {
|
|
544
|
+
changed: { get: () => changed || channel.changed },
|
|
545
|
+
deliveryChanged: { get: () => channel.deliveryChanged },
|
|
1031
546
|
});
|
|
547
|
+
return driver;
|
|
1032
548
|
}
|
|
1033
|
-
/** Explicit import
|
|
1034
|
-
export function initializeSqlite(db, state, revision
|
|
549
|
+
/** Explicit import replay. Caller owns the transaction and format publication. */
|
|
550
|
+
export function initializeSqlite(db, state, revision, lower) {
|
|
1035
551
|
integer(revision);
|
|
1036
552
|
integer(state.runtime.nextActIndex);
|
|
1037
553
|
if (state.hardCap !== null)
|
|
1038
554
|
integer(state.hardCap, 1);
|
|
1039
555
|
if (state.throttlePerMinute !== undefined)
|
|
1040
556
|
integer(state.throttlePerMinute, 1);
|
|
1041
|
-
db
|
|
1042
|
-
|
|
557
|
+
initializeStorageMeta(db, revision);
|
|
558
|
+
lower.initialize(db);
|
|
559
|
+
initializeAgoraSqlite(db, { ...state, preamble: strings(state.preamble), warmup: strings(state.warmup) });
|
|
1043
560
|
const driver = createSqliteDriver(db);
|
|
1044
561
|
for (const activity of state.acts)
|
|
1045
|
-
driver.
|
|
562
|
+
driver.importLanding(activity);
|
|
1046
563
|
requireFact(state.runtime.nextActIndex > driver.header().lastActIndex, 'nextActIndex is behind the activity history.');
|
|
1047
564
|
db.prepare('UPDATE square_meta SET next_act_index=? WHERE id=1').run(state.runtime.nextActIndex);
|
|
1048
|
-
const insert = db.prepare('INSERT INTO receipts(
|
|
565
|
+
const insert = db.prepare('INSERT INTO receipts(owner_key_json,msg_index,at) VALUES(?,?,?)');
|
|
1049
566
|
for (const [owner, observations] of Object.entries(state.runtime.observations)) {
|
|
1050
567
|
string(owner, true);
|
|
1051
568
|
for (const [id, observation] of Object.entries(observations)) {
|
|
@@ -1054,69 +571,79 @@ export function initializeSqlite(db, state, revision = 0) {
|
|
|
1054
571
|
const index = integer(Number(match[1]));
|
|
1055
572
|
requireFact(index < state.runtime.nextActIndex, 'receipt references an unassigned activity index.');
|
|
1056
573
|
requireFact(observation.state === 'seen');
|
|
1057
|
-
|
|
574
|
+
const activity = driver.activity(index);
|
|
575
|
+
if (activity?.kind !== 'say' || activity.messageIndex === undefined)
|
|
576
|
+
continue;
|
|
577
|
+
insert.run(encode(owner), activity.messageIndex, finite(observation.at));
|
|
1058
578
|
}
|
|
1059
579
|
}
|
|
1060
580
|
for (const [owner, value] of Object.entries(state.runtime.leases)) {
|
|
1061
581
|
string(owner, true);
|
|
1062
|
-
const l = validateWatchLease(value);
|
|
1063
|
-
db.prepare('INSERT INTO
|
|
582
|
+
const l = validateWatchLease(value), generation = driver.seat(owner)?.generation ?? 'detached';
|
|
583
|
+
db.prepare('INSERT INTO receive_claims(namespace_key_json,owner_key_json,generation_json,claim_id_json,heartbeat_at,expires_at,selector_json,suppressed_json) VALUES(?,?,?,?,?,?,?,?)')
|
|
584
|
+
.run(encode('agora'), encode(owner), encode(generation), encode(l.leaseId), l.heartbeatAt, l.expiresAt, encode({ ...(l.filter === undefined ? {} : { filter: l.filter }) }), encode(catchCoverage(driver, owner, l)));
|
|
1064
585
|
}
|
|
1065
586
|
}
|
|
1066
587
|
export function exportSqliteState(db) {
|
|
1067
588
|
const d = createSqliteDriver(db), h = d.header();
|
|
1068
589
|
const observations = {}, leases = {};
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
590
|
+
// Receipts are message-coordinate now; export them under the establishing say's activity id.
|
|
591
|
+
for (const row of db.prepare('SELECT r.owner_key_json AS owner_json,r.msg_index AS act_index,r.at FROM receipts r').all()) {
|
|
592
|
+
const owner = decoded(row.owner_json, true);
|
|
593
|
+
const say = d.activityByMessage(integer(row.act_index));
|
|
594
|
+
if (say?.kind !== 'say')
|
|
595
|
+
continue;
|
|
1072
596
|
if (!Object.hasOwn(observations, owner))
|
|
1073
|
-
Object.defineProperty(observations, owner, {
|
|
1074
|
-
|
|
1075
|
-
});
|
|
1076
|
-
observations[owner][`act/${index}`] = { state: 'seen', at: finite(row.at) };
|
|
597
|
+
Object.defineProperty(observations, owner, { value: {}, enumerable: true, writable: true });
|
|
598
|
+
observations[owner][formatActivityId(say.index)] = { state: 'seen', at: finite(row.at) };
|
|
1077
599
|
}
|
|
1078
|
-
for (const row of db.prepare(
|
|
1079
|
-
const owner = decoded(row.
|
|
600
|
+
for (const row of db.prepare("SELECT * FROM receive_claims WHERE namespace_key_json=?").all(encode('agora'))) {
|
|
601
|
+
const owner = decoded(row.owner_key_json, true);
|
|
602
|
+
const selector = object(json(row.selector_json));
|
|
1080
603
|
Object.defineProperty(leases, owner, { value: validateWatchLease({
|
|
1081
|
-
leaseId: decoded(row.
|
|
604
|
+
leaseId: decoded(row.claim_id_json, true), heartbeatAt: finite(row.heartbeat_at), expiresAt: finite(row.expires_at), ...(selector.filter === undefined ? {} : { filter: selector.filter })
|
|
1082
605
|
}), enumerable: true });
|
|
1083
606
|
}
|
|
1084
607
|
return {
|
|
1085
|
-
hardCap: h.hardCap, ...(h.throttlePerMinute === undefined ? {} : { throttlePerMinute: h.throttlePerMinute }), preamble: [...h.preamble], warmup: [...h.warmup],
|
|
1086
|
-
|
|
1087
|
-
}
|
|
608
|
+
hardCap: h.hardCap, ...(h.throttlePerMinute === undefined ? {} : { throttlePerMinute: h.throttlePerMinute }), preamble: [...h.preamble], warmup: [...h.warmup],
|
|
609
|
+
acts: db.prepare('SELECT a.*,m.body_json,m.metadata_json FROM activities a LEFT JOIN messages m ON m.msg_index=a.message_index ORDER BY a.act_index').all().map(activityWithContent),
|
|
610
|
+
runtime: { nextActIndex: h.nextActIndex, observations, leases }
|
|
1088
611
|
};
|
|
1089
612
|
}
|
|
1090
|
-
/**
|
|
1091
|
-
export function inspectSqlite(db) {
|
|
1092
|
-
return inspectRows(db,
|
|
613
|
+
/** Read-only integrity inspection; maintained-index replay runs when the lower schema is supplied. */
|
|
614
|
+
export function inspectSqlite(db, lower) {
|
|
615
|
+
return inspectRows(db, endpoint, lower);
|
|
1093
616
|
}
|
|
1094
|
-
function inspectRows(db,
|
|
617
|
+
function inspectRows(db, inspectEndpoint, lower) {
|
|
1095
618
|
const check = db.prepare('PRAGMA integrity_check').all();
|
|
1096
619
|
requireFact(check.length === 1 && check[0].integrity_check === 'ok', 'SQLite integrity check failed.');
|
|
1097
620
|
const state = exportSqliteState(db), d = createSqliteDriver(db);
|
|
1098
621
|
d.roster();
|
|
1099
|
-
for (const row of db.prepare('SELECT * FROM
|
|
1100
|
-
|
|
622
|
+
for (const row of db.prepare('SELECT * FROM endpoints').all())
|
|
623
|
+
inspectEndpoint(row);
|
|
1101
624
|
for (const row of db.prepare('SELECT * FROM delivery_evidence').all())
|
|
1102
625
|
deliveryEvidence(row);
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
626
|
+
for (const row of db.prepare('SELECT * FROM delivery_deferrals').all())
|
|
627
|
+
deliveryDeferral(row);
|
|
628
|
+
for (const row of db.prepare('SELECT * FROM receive_claims').all())
|
|
629
|
+
receiveClaim(row);
|
|
630
|
+
for (const row of db.prepare('SELECT ma.*,m.msg_index AS existing_msg_index FROM message_addresses ma LEFT JOIN messages m ON m.msg_index=ma.msg_index').all()) {
|
|
631
|
+
requireFact(row.existing_msg_index !== null, 'message address references a missing message.');
|
|
632
|
+
messageAddress(row);
|
|
1106
633
|
}
|
|
1107
634
|
for (const p of d.roster()) {
|
|
1108
635
|
d.attention(p.name);
|
|
1109
636
|
d.incomingAttention(p.name);
|
|
1110
637
|
}
|
|
1111
638
|
for (const a of state.acts)
|
|
1112
|
-
|
|
1113
|
-
|
|
639
|
+
if (a.kind === 'say')
|
|
640
|
+
d.audience(a.index);
|
|
641
|
+
const standingCount = d.roster().filter(p => p.joined).length;
|
|
642
|
+
if (lower === undefined)
|
|
643
|
+
return { standingCount };
|
|
1114
644
|
const rebuilt = new MemoryDatabase(':memory:');
|
|
1115
645
|
try {
|
|
1116
|
-
initializeSqlite(rebuilt, state, d.header().revision);
|
|
1117
|
-
// A migration may remove the activity that established canonical casing.
|
|
1118
|
-
// Retained participant spelling stays authoritative while replay checks
|
|
1119
|
-
// every derived lifecycle/count fact against surviving activities.
|
|
646
|
+
initializeSqlite(rebuilt, state, d.header().revision, lower);
|
|
1120
647
|
const retainName = rebuilt.prepare('UPDATE participants SET name_json=? WHERE name_key_json=?');
|
|
1121
648
|
for (const p of d.roster())
|
|
1122
649
|
retainName.run(encode(p.name), key(p.name));
|
|
@@ -1125,7 +652,24 @@ function inspectRows(db, inspectSeat) {
|
|
|
1125
652
|
const expected = rebuilt.prepare(`SELECT * FROM ${table} ORDER BY 1,2`).all();
|
|
1126
653
|
requireFact(stableJson(original) === stableJson(expected), `derived ${table} index is inconsistent.`);
|
|
1127
654
|
}
|
|
1128
|
-
|
|
655
|
+
// Only reason-bearing Agora content replays from explicit state. Native Channel messages
|
|
656
|
+
// and their unfenced addresses are not restorable from Agora activities; a null
|
|
657
|
+
// generation stays unfenced here even though current landing would fence a new address.
|
|
658
|
+
const upper = 'JOIN activities a ON a.message_index=ma.msg_index JOIN landed_audience d ON d.act_index=a.act_index';
|
|
659
|
+
for (const [table, select] of [
|
|
660
|
+
['messages', 'SELECT DISTINCT m.* FROM messages m JOIN activities a ON a.message_index=m.msg_index JOIN landed_audience d ON d.act_index=a.act_index'],
|
|
661
|
+
['message_addresses', `SELECT DISTINCT ma.* FROM message_addresses ma ${upper} AND d.recipient_key_json=ma.recipient_key_json`],
|
|
662
|
+
]) {
|
|
663
|
+
const original = db.prepare(`${select} ORDER BY 1,2`).all();
|
|
664
|
+
const expected = rebuilt.prepare(`${select} ORDER BY 1,2`).all();
|
|
665
|
+
const comparable = (rows) => table === 'message_addresses'
|
|
666
|
+
? rows.map(({ generation_json: _generation, recipient_json: _recipient, ...row }) => row) : rows;
|
|
667
|
+
requireFact(stableJson(comparable(original)) === stableJson(comparable(expected)), `derived ${table} index is inconsistent.`);
|
|
668
|
+
}
|
|
669
|
+
const actualAddresses = db.prepare("SELECT a.message_index AS msg_index,ma.recipient_key_json,ma.recipient_order FROM activities a JOIN message_addresses ma ON ma.msg_index=a.message_index WHERE a.kind='say' AND EXISTS (SELECT 1 FROM landed_audience d WHERE d.act_index=a.act_index) ORDER BY 1,2").all();
|
|
670
|
+
const expectedAddresses = db.prepare("SELECT a.message_index AS msg_index,d.recipient_key_json,d.recipient_order FROM activities a JOIN landed_audience d ON d.act_index=a.act_index WHERE a.kind='say' ORDER BY 1,2").all();
|
|
671
|
+
requireFact(stableJson(actualAddresses) === stableJson(expectedAddresses), 'derived message address identity/order is inconsistent.');
|
|
672
|
+
return { standingCount };
|
|
1129
673
|
}
|
|
1130
674
|
finally {
|
|
1131
675
|
rebuilt.close();
|