@astrosheep/square 0.4.0 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/artifact.d.ts +1 -3
- package/dist/artifact.js +12 -36
- package/dist/attention-presentation.d.ts +2 -22
- package/dist/attention-presentation.js +9 -12
- package/dist/boundary-presentation.d.ts +3 -14
- package/dist/boundary-presentation.js +29 -104
- package/dist/card-presentation.d.ts +1 -0
- package/dist/card-presentation.js +2 -0
- package/dist/catch-decisions.js +1 -7
- package/dist/claude-boundary-state.d.ts +20 -0
- package/dist/claude-boundary-state.js +84 -0
- package/dist/claude-hook.d.ts +2 -0
- package/dist/claude-hook.js +231 -0
- package/dist/claude-mcp-context.d.ts +1 -1
- package/dist/claude-mcp-context.js +12 -3
- package/dist/cli/context.js +7 -9
- package/dist/cli/observation-commands.d.ts +4 -1
- package/dist/cli/observation-commands.js +37 -4
- package/dist/cli/registry.js +5 -2
- package/dist/codex-address.d.ts +2 -2
- package/dist/codex-boundary-state.d.ts +1 -5
- package/dist/codex-boundary-state.js +0 -4
- package/dist/codex-hook.d.ts +7 -10
- package/dist/codex-hook.js +69 -33
- package/dist/codex-queue.d.ts +10 -11
- package/dist/codex-queue.js +32 -32
- package/dist/codex-watch.d.ts +2 -0
- package/dist/codex-watch.js +100 -0
- package/dist/decisions.js +2 -2
- package/dist/delivery-deferral.d.ts +8 -17
- package/dist/delivery-deferral.js +0 -28
- package/dist/delivery-frame.d.ts +7 -0
- package/dist/delivery-frame.js +11 -0
- package/dist/delivery-ledger-sqlite.d.ts +97 -0
- package/dist/delivery-ledger-sqlite.js +496 -0
- package/dist/delivery-ledger.d.ts +196 -0
- package/dist/delivery-ledger.js +268 -0
- package/dist/delivery-lifetime.d.ts +6 -0
- package/dist/delivery-lifetime.js +45 -0
- package/dist/harness-claude.js +3 -4
- package/dist/harness-links.js +63 -7
- package/dist/harness.js +12 -17
- package/dist/help.js +4 -1
- package/dist/http-delivery.d.ts +5 -9
- package/dist/http-delivery.js +20 -77
- package/dist/inbox.d.ts +6 -49
- package/dist/inbox.js +24 -371
- package/dist/index.d.ts +6 -3
- package/dist/index.js +4 -1
- package/dist/mcp.js +2 -3
- package/dist/model.d.ts +7 -47
- package/dist/model.js +1 -7
- package/dist/notifications.d.ts +24 -20
- package/dist/notifications.js +39 -108
- package/dist/operation-control.d.ts +4 -0
- package/dist/operation-control.js +20 -0
- package/dist/packages/agent-delivery/src/claude-native.d.ts +2 -0
- package/dist/packages/agent-delivery/src/claude-native.js +5 -0
- package/dist/packages/agent-delivery/src/codex-boundary.d.ts +11 -6
- package/dist/packages/agent-delivery/src/codex-boundary.js +31 -40
- package/dist/packages/agent-delivery/src/codex-hook.d.ts +17 -12
- package/dist/packages/agent-delivery/src/codex-hook.js +26 -39
- package/dist/packages/agent-delivery/src/codex.d.ts +3 -0
- package/dist/packages/agent-delivery/src/codex.js +41 -4
- package/dist/packages/agent-delivery/src/index.d.ts +3 -1
- package/dist/packages/agent-delivery/src/index.js +1 -1
- package/dist/packages/agent-delivery/src/paseo.js +11 -2
- package/dist/packages/agent-delivery/src/types.d.ts +1 -0
- package/dist/paseo-delivery.d.ts +5 -5
- package/dist/paseo-delivery.js +19 -97
- package/dist/paseo.d.ts +1 -1
- package/dist/paseo.js +1 -1
- package/dist/plugins/opencode/index.d.ts +5 -3
- package/dist/plugins/opencode/index.js +99 -21
- package/dist/plugins/opencode-shared/delivery.d.ts +23 -0
- package/dist/plugins/opencode-shared/delivery.js +109 -0
- package/dist/plugins/opencode-v1/index.d.ts +28 -0
- package/dist/plugins/opencode-v1/index.js +41 -0
- package/dist/plugins/opencode-v1/runtime.d.ts +56 -0
- package/dist/plugins/opencode-v1/runtime.js +152 -0
- package/dist/plugins/opencode-v1/transport.d.ts +13 -0
- package/dist/plugins/opencode-v1/transport.js +50 -0
- package/dist/plugins/paseo/shared/card-presentation.d.ts +10 -0
- package/dist/plugins/paseo/shared/card-presentation.js +7 -0
- package/dist/ports.d.ts +31 -90
- package/dist/session-lifecycle.d.ts +8 -7
- package/dist/session-lifecycle.js +40 -42
- package/dist/sqlite-square.d.ts +14 -23
- package/dist/sqlite-square.js +132 -139
- package/dist/square-api.js +4 -7
- package/dist/square-core.d.ts +0 -1
- package/dist/square-domain.d.ts +5 -17
- package/dist/square-domain.js +104 -221
- package/dist/square-facade.d.ts +18 -15
- package/dist/square-post.d.ts +53 -0
- package/dist/square-post.js +4 -0
- package/dist/square-posting.d.ts +13 -0
- package/dist/square-posting.js +174 -0
- package/dist/square-producer-sqlite.d.ts +11 -0
- package/dist/square-producer-sqlite.js +210 -0
- package/dist/square-sqlite.d.ts +15 -25
- package/dist/square-sqlite.js +150 -226
- package/dist/views.d.ts +2 -2
- package/dist/views.js +0 -1
- package/dist/wake-sink.d.ts +4 -11
- package/dist/wake-sink.js +8 -32
- package/dist/wake-transports.d.ts +4 -7
- package/dist/wake-transports.js +7 -39
- package/examples/symmetric-domains.mjs +29 -249
- package/guides/change-waits.md +13 -53
- package/guides/claude-native.md +7 -171
- package/guides/codex-delivery.md +55 -80
- package/guides/delivery-primitives.md +48 -207
- package/guides/mcp.md +1 -1
- package/guides/operation-cancellation.md +2 -5
- package/guides/participant-ownership.md +25 -89
- package/guides/pi-native-delivery.md +19 -56
- package/guides/pi-tools.md +6 -8
- package/guides/square-api.md +113 -615
- package/package.json +8 -8
- package/plugins/claude/.claude-plugin/plugin.json +2 -3
- package/plugins/claude/hooks/hooks.json +11 -1
- package/plugins/codex/.codex-plugin/plugin.json +1 -1
- package/plugins/codex/hooks/hooks.json +11 -23
- package/plugins/opencode/index.ts +91 -23
- package/plugins/opencode-shared/delivery.ts +91 -0
- package/plugins/opencode-v1/index.ts +36 -0
- package/plugins/opencode-v1/runtime.ts +134 -0
- package/plugins/opencode-v1/transport.ts +41 -0
- package/plugins/paseo/OVERVIEW.md +5 -5
- package/plugins/paseo/README.md +7 -6
- package/plugins/paseo/client/markdown.tsx +5 -5
- package/plugins/paseo/client/square-message.tsx +28 -24
- package/plugins/paseo/client/transform.ts +8 -5
- package/plugins/paseo/shared/card-presentation.ts +16 -0
- package/plugins/paseo/shared/message.ts +6 -11
- package/plugins/pi/square-pi.js +156 -417
- package/dist/channel-artifact.d.ts +0 -10
- package/dist/channel-artifact.js +0 -55
- package/dist/channel-delivery.d.ts +0 -11
- package/dist/channel-delivery.js +0 -51
- package/dist/channel-domain.d.ts +0 -3
- package/dist/channel-domain.js +0 -83
- package/dist/channel-facade.d.ts +0 -76
- package/dist/channel-facade.js +0 -1
- package/dist/channel-internal.d.ts +0 -126
- package/dist/channel-internal.js +0 -440
- package/dist/channel-metadata.d.ts +0 -11
- package/dist/channel-metadata.js +0 -1
- package/dist/channel-ports.d.ts +0 -241
- package/dist/channel-ports.js +0 -1
- package/dist/channel-presentation.d.ts +0 -17
- package/dist/channel-presentation.js +0 -22
- package/dist/channel-sqlite-schema.d.ts +0 -13
- package/dist/channel-sqlite-schema.js +0 -81
- package/dist/channel-transport.d.ts +0 -62
- package/dist/channel-transport.js +0 -1
- package/dist/channel.d.ts +0 -84
- package/dist/channel.js +0 -60
- package/dist/claude-delivery-status.d.ts +0 -13
- package/dist/claude-delivery-status.js +0 -46
- package/dist/claude-delivery.d.ts +0 -12
- package/dist/claude-delivery.js +0 -108
- package/dist/claude-mod.d.ts +0 -8
- package/dist/claude-mod.js +0 -172
- package/dist/claude-transport.d.ts +0 -5
- package/dist/claude-transport.js +0 -58
- package/dist/cmd/notify-once.d.ts +0 -2
- package/dist/cmd/notify-once.js +0 -29
- package/dist/delivery-attempts.d.ts +0 -52
- package/dist/delivery-attempts.js +0 -73
- package/dist/delivery-effects.d.ts +0 -139
- package/dist/delivery-effects.js +0 -163
- package/dist/delivery-health.d.ts +0 -51
- package/dist/delivery-health.js +0 -173
- package/dist/delivery-operations.d.ts +0 -22
- package/dist/delivery-operations.js +0 -198
- package/dist/delivery-primitives.d.ts +0 -5
- package/dist/delivery-primitives.js +0 -3
- package/dist/delivery.d.ts +0 -56
- package/dist/delivery.js +0 -80
- package/dist/plugins/opencode/delivery.d.ts +0 -19
- package/dist/plugins/opencode/delivery.js +0 -223
- package/dist/presentation-operations.d.ts +0 -5
- package/dist/presentation-operations.js +0 -86
- package/dist/sqlite-channel.d.ts +0 -15
- package/dist/sqlite-channel.js +0 -600
- package/dist/square-index.d.ts +0 -16
- package/dist/square-index.js +0 -106
- package/dist/wake-preview.d.ts +0 -7
- package/dist/wake-preview.js +0 -15
- package/plugins/claude/hooks/register.js +0 -132
- package/plugins/claude/types.d.ts +0 -7
- package/plugins/opencode/delivery.ts +0 -198
- /package/dist/{channel-values.d.ts → square-values.d.ts} +0 -0
- /package/dist/{channel-values.js → square-values.js} +0 -0
package/dist/square-sqlite.js
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
import { DatabaseSync as MemoryDatabase } from 'node:sqlite';
|
|
2
1
|
import { validateActivityMetadata, activityMetadataHasContent } from './model.js';
|
|
3
|
-
import {
|
|
2
|
+
import { deriveLanding, landingFacts } from './square-domain.js';
|
|
4
3
|
import { formatActivityId } from './square-core.js';
|
|
5
4
|
import { compileSearchPattern } from './search.js';
|
|
6
|
-
import {
|
|
5
|
+
import { validateEndpointGeneration as endpointGeneration } from './message.js';
|
|
6
|
+
import { createProducerDriver, producerSchemaSql, initializeProducerSqlite, resetProducerSqlite } from './square-producer-sqlite.js';
|
|
7
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,
|
|
8
|
+
import { encode, key, invalidArtifact, requireFact, integer, finite, string, json, object, shape, strings, decoded, immutable, pageRows, pageLimit } from './sqlite-values.js';
|
|
9
9
|
export { invalidArtifact } from './sqlite-values.js';
|
|
10
|
-
export { endpointGeneration } from './
|
|
10
|
+
export { validateEndpointGeneration as endpointGeneration } from './message.js';
|
|
11
11
|
export const APPLICATION_ID = 0x53515245;
|
|
12
|
-
export const CURRENT_USER_VERSION =
|
|
12
|
+
export const CURRENT_USER_VERSION = 12;
|
|
13
13
|
export function emptyRuntimeState(nextActIndex = 0) {
|
|
14
14
|
return { nextActIndex, observations: {}, leases: {} };
|
|
15
15
|
}
|
|
16
16
|
const ACTIVITY_KIND_CHECK = "kind IN ('join','done','say','read','listen','ignore','unlisten')";
|
|
17
|
-
/**
|
|
17
|
+
/** Square owns activity, audience, participant, and read-state facts. */
|
|
18
18
|
export const AGORA_SCHEMA_SQL = `CREATE TABLE square_meta (
|
|
19
19
|
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
20
20
|
next_act_index INTEGER NOT NULL CHECK (next_act_index BETWEEN 0 AND 9007199254740991),
|
|
@@ -31,9 +31,7 @@ CREATE TABLE activities (
|
|
|
31
31
|
actor_key_json TEXT COLLATE BINARY NOT NULL,
|
|
32
32
|
reach TEXT CHECK (reach = 'bell'),
|
|
33
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),
|
|
35
34
|
payload_json TEXT NOT NULL CHECK (json_valid(payload_json)),
|
|
36
|
-
CHECK ((kind = 'say') = (message_index IS NOT NULL)),
|
|
37
35
|
CHECK (kind = 'say' OR (reach IS NULL AND reply_index IS NULL))
|
|
38
36
|
);
|
|
39
37
|
CREATE INDEX activity_kind_index ON activities(kind, act_index);
|
|
@@ -41,7 +39,6 @@ CREATE INDEX activity_actor_index ON activities(actor_key_json, act_index);
|
|
|
41
39
|
CREATE INDEX activity_say_actor_index ON activities(actor_key_json, act_index) WHERE kind='say';
|
|
42
40
|
CREATE INDEX activity_say_time ON activities(at, act_index) WHERE kind='say';
|
|
43
41
|
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;
|
|
45
42
|
CREATE TABLE participants (
|
|
46
43
|
name_key_json TEXT COLLATE BINARY PRIMARY KEY NOT NULL,
|
|
47
44
|
name_json TEXT NOT NULL,
|
|
@@ -67,31 +64,47 @@ CREATE TABLE landed_audience (
|
|
|
67
64
|
recipient_key_json TEXT COLLATE BINARY NOT NULL,
|
|
68
65
|
recipient_order INTEGER NOT NULL CHECK (recipient_order >= 0),
|
|
69
66
|
reason TEXT NOT NULL CHECK (reason IN ('mention','reply','attention','bell')),
|
|
67
|
+
owner_json TEXT,
|
|
68
|
+
generation_json TEXT,
|
|
70
69
|
PRIMARY KEY (act_index, recipient_key_json),
|
|
71
70
|
UNIQUE (act_index, recipient_order)
|
|
72
71
|
) WITHOUT ROWID;
|
|
73
|
-
CREATE INDEX audience_recipient ON landed_audience(recipient_key_json, act_index)
|
|
72
|
+
CREATE INDEX audience_recipient ON landed_audience(recipient_key_json, act_index);
|
|
73
|
+
CREATE TABLE participant_seats (
|
|
74
|
+
participant_key_json TEXT PRIMARY KEY NOT NULL, participant_json TEXT NOT NULL,
|
|
75
|
+
owner_json TEXT NOT NULL, generation_json TEXT NOT NULL,
|
|
76
|
+
wake_address_json TEXT, attachment TEXT NOT NULL CHECK (attachment IN ('attached','detached'))
|
|
77
|
+
) WITHOUT ROWID;
|
|
78
|
+
CREATE INDEX participant_seat_owner ON participant_seats(owner_json);
|
|
79
|
+
CREATE TABLE caught_activity (
|
|
80
|
+
recipient_key_json TEXT NOT NULL, act_index INTEGER NOT NULL, at REAL NOT NULL,
|
|
81
|
+
PRIMARY KEY (recipient_key_json,act_index)
|
|
82
|
+
) WITHOUT ROWID;
|
|
83
|
+
CREATE TABLE catch_leases (
|
|
84
|
+
recipient_key_json TEXT PRIMARY KEY NOT NULL, generation_json TEXT NOT NULL,
|
|
85
|
+
lease_json TEXT NOT NULL CHECK (json_valid(lease_json))
|
|
86
|
+
) WITHOUT ROWID;`;
|
|
74
87
|
export function initializeAgoraSqlite(db, configuration = {}) {
|
|
75
88
|
db.exec(AGORA_SCHEMA_SQL);
|
|
76
89
|
db.prepare('INSERT INTO square_meta(id,next_act_index,hard_cap,throttle_per_minute,preamble_json,warmup_json) VALUES(1,0,?,?,?,?)')
|
|
77
90
|
.run(configuration.hardCap ?? null, configuration.throttlePerMinute ?? null, JSON.stringify(configuration.preamble ?? []), JSON.stringify(configuration.warmup ?? []));
|
|
78
91
|
}
|
|
79
92
|
export function resetAgoraSqlite(db) {
|
|
80
|
-
for (const table of ['landed_audience', 'attention', 'participants', 'activities', 'square_meta'])
|
|
93
|
+
for (const table of ['catch_leases', 'caught_activity', 'participant_seats', 'landed_audience', 'attention', 'participants', 'activities', 'square_meta'])
|
|
81
94
|
db.exec(`DROP TABLE ${table}`);
|
|
82
95
|
}
|
|
83
|
-
function validateSquareFraming(db,
|
|
96
|
+
function validateSquareFraming(db, location = '') {
|
|
84
97
|
const identity = db.prepare('SELECT application_id, user_version FROM pragma_application_id, pragma_user_version').get();
|
|
85
98
|
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-
|
|
87
|
-
validateSchema(db, [STORAGE_META_SQL,
|
|
99
|
+
requireFact(identity.user_version === CURRENT_USER_VERSION, 'unsupported SQLite format version; this build opens only fresh format-12 squares.');
|
|
100
|
+
validateSchema(db, [STORAGE_META_SQL, producerSchemaSql, AGORA_SCHEMA_SQL].join('\n'), location);
|
|
88
101
|
}
|
|
89
|
-
/**
|
|
90
|
-
export function squareStoreFormat(
|
|
102
|
+
/** Full Square framing and reset, including producer state. */
|
|
103
|
+
export function squareStoreFormat(resetStorage) {
|
|
91
104
|
return {
|
|
92
105
|
applicationId: APPLICATION_ID, userVersion: CURRENT_USER_VERSION, observerKey: 'square',
|
|
93
|
-
validate: (db, location) => validateSquareFraming(db,
|
|
94
|
-
reset(db) {
|
|
106
|
+
validate: (db, location) => validateSquareFraming(db, location),
|
|
107
|
+
reset(db) { resetProducerSqlite(db); resetAgoraSqlite(db); resetStorage(db); },
|
|
95
108
|
};
|
|
96
109
|
}
|
|
97
110
|
const kinds = ['join', 'done', 'say', 'read', 'listen', 'ignore', 'unlisten'];
|
|
@@ -103,14 +116,11 @@ function head(row) {
|
|
|
103
116
|
requireFact(row.reach === null || row.kind === 'say' && row.reach === 'bell');
|
|
104
117
|
if (row.reply_index !== null)
|
|
105
118
|
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));
|
|
108
119
|
return immutable({
|
|
109
120
|
index, at, kind: row.kind, actor, ...(row.reach === null ? {} : { reach: 'bell' }), ...(row.reply_index === null ? {} : { reply: row.reply_index }),
|
|
110
|
-
...(messageIndex === undefined ? {} : { messageIndex }),
|
|
111
121
|
});
|
|
112
122
|
}
|
|
113
|
-
/**
|
|
123
|
+
/** Validate the complete persisted activity. */
|
|
114
124
|
export function validateStoredActivity(value) {
|
|
115
125
|
const a = object(value);
|
|
116
126
|
integer(a.index);
|
|
@@ -120,15 +130,24 @@ export function validateStoredActivity(value) {
|
|
|
120
130
|
string(a.actor, true);
|
|
121
131
|
switch (a.kind) {
|
|
122
132
|
case 'say':
|
|
123
|
-
shape(a, [...common], ['mentions', 'reach', 'reply', '
|
|
124
|
-
|
|
133
|
+
shape(a, [...common], ['mentions', 'reach', 'reply', 'body', 'metadata']);
|
|
134
|
+
if (a.body !== undefined)
|
|
135
|
+
string(a.body);
|
|
136
|
+
if (a.metadata !== undefined) {
|
|
137
|
+
try {
|
|
138
|
+
validateActivityMetadata(a.metadata);
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
throw invalidArtifact('say metadata is malformed.');
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
requireFact((typeof a.body === 'string' && a.body.trim() !== '') || activityMetadataHasContent(a.metadata), 'say must contain a non-empty body or non-empty metadata.');
|
|
125
145
|
if (Object.hasOwn(a, 'mentions'))
|
|
126
146
|
strings(a.mentions);
|
|
127
147
|
if (Object.hasOwn(a, 'reach'))
|
|
128
148
|
requireFact(a.reach === 'bell');
|
|
129
149
|
if (Object.hasOwn(a, 'reply'))
|
|
130
150
|
requireFact(integer(a.reply) < a.index);
|
|
131
|
-
integer(a.messageIndex);
|
|
132
151
|
break;
|
|
133
152
|
case 'done':
|
|
134
153
|
shape(a, common, ['body']);
|
|
@@ -149,44 +168,24 @@ export function validateStoredActivity(value) {
|
|
|
149
168
|
}
|
|
150
169
|
return value;
|
|
151
170
|
}
|
|
152
|
-
/**
|
|
171
|
+
/** Validate complete activity content. */
|
|
153
172
|
export function validateHydratedActivity(value) {
|
|
154
173
|
const a = object(value);
|
|
155
174
|
if (a.kind !== 'say')
|
|
156
175
|
return validateStoredActivity(a);
|
|
157
|
-
|
|
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 }) };
|
|
176
|
+
return validateStoredActivity(a);
|
|
171
177
|
}
|
|
172
|
-
/**
|
|
178
|
+
/** Decode an activity with its complete payload. */
|
|
173
179
|
function activity(row) {
|
|
174
180
|
const h = head(row), payload = object(json(row.payload_json));
|
|
175
|
-
// Payload cannot shadow indexed structural columns
|
|
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'] : [];
|
|
181
|
+
// Payload cannot shadow indexed structural columns.
|
|
182
|
+
const allowed = h.kind === 'say' ? ['mentions', 'body', 'metadata'] : h.kind === 'done' ? ['body'] : h.kind === 'read' ? ['through'] : h.kind === 'listen' || h.kind === 'ignore' || h.kind === 'unlisten' ? ['target'] : [];
|
|
177
183
|
requireFact(Object.keys(payload).every(k => allowed.includes(k)));
|
|
178
184
|
return immutable(validateStoredActivity({ ...h, ...payload }));
|
|
179
185
|
}
|
|
180
|
-
/**
|
|
186
|
+
/** Activity and content are one stored fact. */
|
|
181
187
|
function activityWithContent(row) {
|
|
182
|
-
|
|
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 }) }));
|
|
188
|
+
return activity(row);
|
|
190
189
|
}
|
|
191
190
|
function participant(row) {
|
|
192
191
|
const name = decoded(row.name_json, true);
|
|
@@ -254,8 +253,7 @@ function activitySelection(filter, alias = 'a') {
|
|
|
254
253
|
terms.push(`NOT EXISTS (
|
|
255
254
|
SELECT 1 FROM json_each(?) field WHERE NOT EXISTS (
|
|
256
255
|
WITH RECURSIVE walk(value,type,depth) AS (
|
|
257
|
-
SELECT
|
|
258
|
-
CASE WHEN ${alias}.kind='say' THEN json_type(m.metadata_json,'$') ELSE json_type(${alias}.payload_json,'$.metadata') END,0
|
|
256
|
+
SELECT json_extract(${alias}.payload_json,'$.metadata'), json_type(${alias}.payload_json,'$.metadata'),0
|
|
259
257
|
UNION ALL
|
|
260
258
|
SELECT child.value,child.type,walk.depth+1
|
|
261
259
|
FROM walk JOIN json_each(CASE WHEN walk.type='object' THEN walk.value ELSE '{}' END) child
|
|
@@ -271,12 +269,12 @@ function activitySelection(filter, alias = 'a') {
|
|
|
271
269
|
args.push(encode(filter.metadataMatch.fields));
|
|
272
270
|
}
|
|
273
271
|
if (filter.search !== undefined) {
|
|
274
|
-
terms.push(`square_search(?,?,'act/'||${alias}.act_index,${alias}.actor_json,
|
|
272
|
+
terms.push(`square_search(?,?,'act/'||${alias}.act_index,${alias}.actor_json,json_extract(${alias}.payload_json,'$.body'))=1`);
|
|
275
273
|
args.push(filter.search.mode, filter.search.text);
|
|
276
274
|
}
|
|
277
275
|
return { sql: terms.length ? terms.join(' AND ') : '1', args };
|
|
278
276
|
}
|
|
279
|
-
export function createSqliteDriver(db
|
|
277
|
+
export function createSqliteDriver(db) {
|
|
280
278
|
let changed = false;
|
|
281
279
|
const all = (sql, args = []) => db.prepare(sql).all(...args);
|
|
282
280
|
const one = (sql, args = []) => db.prepare(sql).get(...args);
|
|
@@ -304,41 +302,34 @@ export function createSqliteDriver(db, channelDriver = createChannelSqlite(db))
|
|
|
304
302
|
return activitySelection(filter);
|
|
305
303
|
}
|
|
306
304
|
const read = {};
|
|
307
|
-
const
|
|
308
|
-
const currentEndpoint = (name) =>
|
|
309
|
-
|
|
305
|
+
const producer = createProducerDriver(db);
|
|
306
|
+
const currentEndpoint = (name) => {
|
|
307
|
+
const row = one('SELECT * FROM participant_seats WHERE participant_key_json=?', [key(name)]);
|
|
308
|
+
if (!row)
|
|
309
|
+
return undefined;
|
|
310
|
+
return immutable({ participant: decoded(row.participant_json, true), owner: decoded(row.owner_json, true), generation: endpointGeneration(decoded(row.generation_json, true)), attachment: row.attachment, ...(row.wake_address_json === null ? {} : { wakeAddress: json(row.wake_address_json) }) });
|
|
311
|
+
};
|
|
310
312
|
function directedSelection(recipient, filter) {
|
|
311
313
|
const participant = read.participant(recipient), base = selectActivity({ ...filter, actors: filter.senders });
|
|
312
314
|
base.sql += ' AND d.recipient_key_json=?';
|
|
313
315
|
base.args.push(participant ? key(participant.name) : '');
|
|
314
316
|
if (filter.currentGeneration) {
|
|
315
|
-
base.sql += ' AND (
|
|
317
|
+
base.sql += ' AND (d.generation_json IS NULL OR d.generation_json=?)';
|
|
316
318
|
base.args.push(participant ? encode(currentEndpoint(participant.name)?.generation ?? '') : '');
|
|
317
319
|
}
|
|
318
320
|
if (filter.unseenOnly) {
|
|
319
|
-
base.sql += ' AND NOT EXISTS (SELECT 1 FROM
|
|
320
|
-
base.args.push(
|
|
321
|
+
base.sql += ' AND NOT EXISTS (SELECT 1 FROM caught_activity r WHERE r.recipient_key_json=? AND r.act_index=a.act_index)';
|
|
322
|
+
base.args.push(key(participant?.name ?? recipient));
|
|
321
323
|
}
|
|
322
324
|
if (filter.attentionOnly)
|
|
323
325
|
base.sql += " AND d.reason IN ('mention','reply','bell')";
|
|
324
326
|
return { base, participant };
|
|
325
327
|
}
|
|
326
328
|
// ----- Upper Agora read capabilities -----
|
|
327
|
-
const lower = channelRead;
|
|
328
329
|
const readMethods = {
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
deliveryByToken: (token) => lower.deliveryByToken(token),
|
|
333
|
-
cancelledThrough(participant, generation) {
|
|
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
|
-
},
|
|
330
|
+
pendingPosts: producer.pendingPosts, postedPosts: producer.postedPosts, ownsPost: producer.ownsPost,
|
|
331
|
+
seat: currentEndpoint,
|
|
332
|
+
seatsForOwner: requested => immutable(all('SELECT participant_json FROM participant_seats WHERE owner_json=?', [encode(requested)]).map(row => currentEndpoint(decoded(row.participant_json, true)))),
|
|
342
333
|
header() { return readSquareHeader(db); },
|
|
343
334
|
participant(name) { const r = one('SELECT * FROM participants WHERE name_key_json=?', [key(name)]); return r ? participant(r) : undefined; },
|
|
344
335
|
participantCounts() {
|
|
@@ -352,19 +343,15 @@ export function createSqliteDriver(db, channelDriver = createChannelSqlite(db))
|
|
|
352
343
|
const r = one('SELECT * FROM activities WHERE act_index=?', [integer(index)]);
|
|
353
344
|
return r ? activity(r) : undefined;
|
|
354
345
|
},
|
|
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
|
-
},
|
|
359
346
|
activityHeads(filter, page) {
|
|
360
347
|
const s = selectActivity(filter), limit = pageLimit(page);
|
|
361
|
-
return pageRows(all(`SELECT a
|
|
348
|
+
return pageRows(all(`SELECT a.* FROM activities a WHERE ${s.sql} ORDER BY a.act_index ${page.edge === 'newest' ? 'DESC' : 'ASC'} LIMIT ?`, [...s.args, limit]), page, head);
|
|
362
349
|
},
|
|
363
350
|
activities(indexes) {
|
|
364
351
|
indexes.forEach(v => integer(v));
|
|
365
|
-
return immutable(all('SELECT a
|
|
352
|
+
return immutable(all('SELECT a.* FROM activities a WHERE a.act_index IN (SELECT value FROM json_each(?)) ORDER BY a.act_index', [encode(indexes)]).map(activityWithContent));
|
|
366
353
|
},
|
|
367
|
-
activityCount(filter) { const s = selectActivity(filter); return integer(one(`SELECT COUNT(*) AS n FROM activities a
|
|
354
|
+
activityCount(filter) { const s = selectActivity(filter); return integer(one(`SELECT COUNT(*) AS n FROM activities a WHERE ${s.sql}`, s.args).n); },
|
|
368
355
|
contextIndexes(centers, before, after) {
|
|
369
356
|
integer(before);
|
|
370
357
|
integer(after);
|
|
@@ -406,62 +393,66 @@ export function createSqliteDriver(db, channelDriver = createChannelSqlite(db))
|
|
|
406
393
|
args.push(integer(take));
|
|
407
394
|
return immutable(all(`SELECT at FROM activities WHERE ${where} ORDER BY at DESC,act_index DESC LIMIT ?`, args).map(r => finite(r.at)).reverse());
|
|
408
395
|
},
|
|
409
|
-
audience(index) { return immutable(all('SELECT p.name_json,d.reason,
|
|
396
|
+
audience(index) { return immutable(all('SELECT p.name_json,d.reason,d.generation_json,d.owner_json FROM landed_audience d JOIN participants p ON p.name_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)) }), ...(r.owner_json === null ? {} : { owner: decoded(r.owner_json, true) }) }))); },
|
|
410
397
|
directed(recipient, filter, page) {
|
|
411
398
|
const { base, participant: p } = directedSelection(recipient, filter), limit = pageLimit(page);
|
|
412
|
-
const rows = all(`SELECT a.*,
|
|
399
|
+
const rows = all(`SELECT a.*,d.reason,d.generation_json FROM activities a JOIN landed_audience d ON d.act_index=a.act_index WHERE ${base.sql} ORDER BY a.act_index ${page.edge === 'newest' ? 'DESC' : 'ASC'} LIMIT ?`, [...base.args, limit]);
|
|
413
400
|
return pageRows(rows, page, r => {
|
|
414
401
|
const h = head(r);
|
|
415
402
|
return { index: h.index, at: h.at, actor: h.actor, recipient: p?.name ?? recipient,
|
|
416
|
-
route: routeKind(r.reason),
|
|
403
|
+
route: routeKind(r.reason),
|
|
417
404
|
...(r.generation_json === null ? {} : { generation: endpointGeneration(decoded(r.generation_json, true)) }) };
|
|
418
405
|
});
|
|
419
406
|
},
|
|
420
407
|
directedAt(recipient, index) { return read.directed(recipient, { afterIndex: integer(index) - 1, throughIndex: index }, { edge: 'oldest', limit: 1 }).items[0]; },
|
|
421
408
|
directedCount(recipient, filter) {
|
|
422
409
|
const { base } = directedSelection(recipient, filter);
|
|
423
|
-
return integer(one(`SELECT COUNT(*) AS n FROM activities a
|
|
410
|
+
return integer(one(`SELECT COUNT(*) AS n FROM activities a JOIN landed_audience d ON d.act_index=a.act_index WHERE ${base.sql}`, base.args).n);
|
|
424
411
|
},
|
|
425
412
|
receipt(name, index) {
|
|
426
|
-
const r = one('SELECT
|
|
413
|
+
const r = one('SELECT at FROM caught_activity WHERE recipient_key_json=? AND act_index=?', [key(name), integer(index)]);
|
|
427
414
|
return r ? immutable({ at: finite(r.at) }) : undefined;
|
|
428
415
|
},
|
|
429
416
|
receiptFacts(name, afterIndex) {
|
|
430
|
-
const k =
|
|
431
|
-
const r = one('SELECT MAX(at) AS latest FROM
|
|
432
|
-
const e = one('SELECT MAX(
|
|
417
|
+
const k = key(name);
|
|
418
|
+
const r = one('SELECT MAX(at) AS latest FROM caught_activity WHERE recipient_key_json=?', [k]);
|
|
419
|
+
const e = one('SELECT MAX(act_index) AS latest FROM caught_activity WHERE recipient_key_json=? AND act_index>?', [k, integer(afterIndex, -1)]);
|
|
433
420
|
return immutable({ ...(r.latest === null ? {} : { latestAt: finite(r.latest) }), ...(e.latest === null ? {} : { latestActorActivityIndex: integer(e.latest) }) });
|
|
434
421
|
},
|
|
435
422
|
lease(name) {
|
|
436
|
-
const
|
|
437
|
-
|
|
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));
|
|
423
|
+
const row = one('SELECT lease_json FROM catch_leases WHERE recipient_key_json=?', [key(name)]);
|
|
424
|
+
return row ? immutable(validateWatchLease(json(row.lease_json))) : undefined;
|
|
444
425
|
},
|
|
445
|
-
deliveryDeferrals: (participant, generation) => lower.deliveryDeferrals(participant, generation),
|
|
446
426
|
};
|
|
447
|
-
Object.assign(read, readMethods
|
|
427
|
+
Object.assign(read, readMethods);
|
|
448
428
|
// ----- Upper Agora writes / landing -----
|
|
449
429
|
const writes = {
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
430
|
+
insertNotice: producer.insertNotice, ackMatching: producer.ackMatching,
|
|
431
|
+
acquirePost: producer.acquirePost, settlePost: producer.settlePost, foldReceipt: producer.foldReceipt,
|
|
432
|
+
setSeatAttachment(seat, attachment) {
|
|
433
|
+
const count = run('UPDATE participant_seats SET attachment=? WHERE participant_key_json=? AND owner_json=? AND generation_json=? AND attachment<>?', [attachment, key(seat.participant), encode(seat.owner), encode(seat.generation), attachment]);
|
|
434
|
+
changed ||= count > 0;
|
|
435
|
+
return count > 0;
|
|
436
|
+
},
|
|
437
|
+
storeSeat(value) {
|
|
438
|
+
run('INSERT INTO participant_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), value.wakeAddress === undefined ? null : encode(value.wakeAddress), value.attachment]);
|
|
439
|
+
changed = true;
|
|
440
|
+
return true;
|
|
441
|
+
},
|
|
442
|
+
removeSeat(value) {
|
|
443
|
+
const count = run('DELETE FROM participant_seats WHERE participant_key_json=? AND owner_json=? AND generation_json=?', [key(value.participant), encode(value.owner), encode(value.generation)]);
|
|
444
|
+
if (count)
|
|
445
|
+
run('DELETE FROM catch_leases WHERE recipient_key_json=? AND generation_json=?', [key(value.participant), encode(value.generation)]);
|
|
446
|
+
changed ||= count > 0;
|
|
447
|
+
return count > 0;
|
|
448
|
+
},
|
|
456
449
|
applyLanding(effect) {
|
|
457
450
|
const a = validateStoredActivity(effect.activity);
|
|
458
451
|
requireFact(a.index >= read.header().nextActIndex && a.index < Number.MAX_SAFE_INTEGER, 'activity index cannot advance.');
|
|
459
|
-
const { index, at, kind, actor, reach, reply,
|
|
460
|
-
|
|
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.
|
|
452
|
+
const { index, at, kind, actor, reach, reply, ...payload } = a;
|
|
453
|
+
run('INSERT INTO activities(act_index,at,kind,actor_json,actor_key_json,reach,reply_index,payload_json) VALUES(?,?,?,?,?,?,?,?)', [index, at, kind, encode(actor), key(actor), reach ?? null, reply ?? null, encode(payload)]);
|
|
463
454
|
effect.audience.forEach((address, order) => {
|
|
464
|
-
run('INSERT INTO landed_audience(act_index,recipient_key_json,recipient_order,reason) VALUES(
|
|
455
|
+
run('INSERT INTO landed_audience(act_index,recipient_key_json,recipient_order,reason,owner_json,generation_json) VALUES(?,?,?,?,?,?)', [index, key(address.recipient), order, address.route, address.owner === undefined ? null : encode(address.owner), address.generation === undefined ? null : encode(address.generation)]);
|
|
465
456
|
});
|
|
466
457
|
if (effect.participant) {
|
|
467
458
|
const p = effect.participant;
|
|
@@ -492,62 +483,54 @@ export function createSqliteDriver(db, channelDriver = createChannelSqlite(db))
|
|
|
492
483
|
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]);
|
|
493
484
|
}
|
|
494
485
|
run('UPDATE square_meta SET next_act_index=? WHERE id=1', [index + 1]);
|
|
495
|
-
|
|
486
|
+
if (kind === 'say')
|
|
487
|
+
producer.consumeActivity(index, at);
|
|
496
488
|
changed = true;
|
|
497
489
|
},
|
|
498
|
-
putLease(name, value, generation
|
|
499
|
-
const l = validateWatchLease(value),
|
|
500
|
-
if (generation !== undefined)
|
|
501
|
-
endpointGeneration(generation);
|
|
502
|
-
const existing = channelRead.claimLease(canonical, 'agora');
|
|
503
|
-
if (existing !== undefined && generation !== undefined && existing.generation !== generation)
|
|
490
|
+
putLease(name, value, generation) {
|
|
491
|
+
const l = validateWatchLease(value), seat = currentEndpoint(name);
|
|
492
|
+
if (!seat || generation !== undefined && generation !== seat.generation)
|
|
504
493
|
return false;
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
suppressed: suppressed ?? [], heartbeatAt: l.heartbeatAt, expiresAt: l.expiresAt });
|
|
494
|
+
run('INSERT INTO catch_leases(recipient_key_json,generation_json,lease_json) VALUES(?,?,?) ON CONFLICT(recipient_key_json) DO UPDATE SET generation_json=excluded.generation_json,lease_json=excluded.lease_json', [key(name), encode(seat.generation), encode(l)]);
|
|
495
|
+
changed = true;
|
|
496
|
+
return true;
|
|
509
497
|
},
|
|
510
498
|
removeLease(name, expected, generation) {
|
|
511
|
-
|
|
512
|
-
if (existing === undefined || existing.id !== expected || generation !== undefined && existing.generation !== generation)
|
|
499
|
+
if (read.lease(name)?.leaseId !== expected || generation !== undefined && currentEndpoint(name)?.generation !== generation)
|
|
513
500
|
return false;
|
|
514
|
-
|
|
501
|
+
const count = run('DELETE FROM catch_leases WHERE recipient_key_json=?', [key(name)]);
|
|
502
|
+
changed ||= count > 0;
|
|
503
|
+
return count > 0;
|
|
504
|
+
},
|
|
505
|
+
markSeen(name, indexes, at) {
|
|
506
|
+
finite(at);
|
|
507
|
+
let count = 0;
|
|
508
|
+
for (const index of indexes)
|
|
509
|
+
count += run('INSERT OR IGNORE INTO caught_activity(recipient_key_json,act_index,at) SELECT recipient_key_json,act_index,? FROM landed_audience WHERE recipient_key_json=? AND act_index=?', [at, key(name), integer(index)]);
|
|
510
|
+
changed ||= count > 0;
|
|
511
|
+
return count;
|
|
515
512
|
},
|
|
516
513
|
};
|
|
517
514
|
function importLanding(activityValue) {
|
|
518
515
|
const validated = validateHydratedActivity(activityValue);
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
driver.
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
const current = channelRead.endpoint(validated.actor);
|
|
527
|
-
if (current !== undefined)
|
|
528
|
-
channelWrite.removeEndpoint(current);
|
|
529
|
-
}
|
|
530
|
-
return validated;
|
|
516
|
+
driver.applyLanding(deriveLanding(validated, landingFacts(read, validated)));
|
|
517
|
+
if (validated.kind === 'join')
|
|
518
|
+
driver.storeSeat({ participant: validated.actor, owner: 'import', generation: formatActivityId(validated.index), attachment: 'attached' });
|
|
519
|
+
else if (validated.kind === 'done') {
|
|
520
|
+
const current = read.seat(validated.actor);
|
|
521
|
+
if (current)
|
|
522
|
+
driver.removeSeat(current);
|
|
531
523
|
}
|
|
532
|
-
|
|
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;
|
|
524
|
+
return validated;
|
|
542
525
|
}
|
|
543
|
-
const driver = Object.defineProperties(Object.assign(read, writes, {
|
|
544
|
-
changed: { get: () => changed ||
|
|
545
|
-
deliveryChanged: { get: () =>
|
|
526
|
+
const driver = Object.defineProperties(Object.assign(read, writes, { importLanding }), {
|
|
527
|
+
changed: { get: () => changed || producer.changed },
|
|
528
|
+
deliveryChanged: { get: () => producer.deliveryChanged },
|
|
546
529
|
});
|
|
547
530
|
return driver;
|
|
548
531
|
}
|
|
549
532
|
/** Explicit import replay. Caller owns the transaction and format publication. */
|
|
550
|
-
export function initializeSqlite(db, state, revision
|
|
533
|
+
export function initializeSqlite(db, state, revision) {
|
|
551
534
|
integer(revision);
|
|
552
535
|
integer(state.runtime.nextActIndex);
|
|
553
536
|
if (state.hardCap !== null)
|
|
@@ -555,14 +538,14 @@ export function initializeSqlite(db, state, revision, lower) {
|
|
|
555
538
|
if (state.throttlePerMinute !== undefined)
|
|
556
539
|
integer(state.throttlePerMinute, 1);
|
|
557
540
|
initializeStorageMeta(db, revision);
|
|
558
|
-
|
|
541
|
+
initializeProducerSqlite(db);
|
|
559
542
|
initializeAgoraSqlite(db, { ...state, preamble: strings(state.preamble), warmup: strings(state.warmup) });
|
|
560
543
|
const driver = createSqliteDriver(db);
|
|
561
544
|
for (const activity of state.acts)
|
|
562
545
|
driver.importLanding(activity);
|
|
563
546
|
requireFact(state.runtime.nextActIndex > driver.header().lastActIndex, 'nextActIndex is behind the activity history.');
|
|
564
547
|
db.prepare('UPDATE square_meta SET next_act_index=? WHERE id=1').run(state.runtime.nextActIndex);
|
|
565
|
-
const insert = db.prepare('INSERT INTO
|
|
548
|
+
const insert = db.prepare('INSERT INTO caught_activity(recipient_key_json,act_index,at) VALUES(?,?,?)');
|
|
566
549
|
for (const [owner, observations] of Object.entries(state.runtime.observations)) {
|
|
567
550
|
string(owner, true);
|
|
568
551
|
for (const [id, observation] of Object.entries(observations)) {
|
|
@@ -572,65 +555,41 @@ export function initializeSqlite(db, state, revision, lower) {
|
|
|
572
555
|
requireFact(index < state.runtime.nextActIndex, 'receipt references an unassigned activity index.');
|
|
573
556
|
requireFact(observation.state === 'seen');
|
|
574
557
|
const activity = driver.activity(index);
|
|
575
|
-
if (activity?.kind !== 'say'
|
|
558
|
+
if (activity?.kind !== 'say')
|
|
576
559
|
continue;
|
|
577
|
-
insert.run(
|
|
560
|
+
insert.run(key(owner), index, finite(observation.at));
|
|
578
561
|
}
|
|
579
562
|
}
|
|
580
563
|
for (const [owner, value] of Object.entries(state.runtime.leases)) {
|
|
581
564
|
string(owner, true);
|
|
582
565
|
const l = validateWatchLease(value), generation = driver.seat(owner)?.generation ?? 'detached';
|
|
583
|
-
db.prepare('INSERT INTO
|
|
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)));
|
|
566
|
+
db.prepare('INSERT INTO catch_leases(recipient_key_json,generation_json,lease_json) VALUES(?,?,?)').run(key(owner), encode(generation), encode(l));
|
|
585
567
|
}
|
|
586
568
|
}
|
|
587
569
|
export function exportSqliteState(db) {
|
|
588
570
|
const d = createSqliteDriver(db), h = d.header();
|
|
589
571
|
const observations = {}, leases = {};
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
const owner = decoded(row.owner_json, true);
|
|
593
|
-
const say = d.activityByMessage(integer(row.act_index));
|
|
594
|
-
if (say?.kind !== 'say')
|
|
595
|
-
continue;
|
|
572
|
+
for (const row of db.prepare('SELECT p.name_json,r.act_index,r.at FROM caught_activity r JOIN participants p ON p.name_key_json=r.recipient_key_json').all()) {
|
|
573
|
+
const owner = decoded(row.name_json, true);
|
|
596
574
|
if (!Object.hasOwn(observations, owner))
|
|
597
575
|
Object.defineProperty(observations, owner, { value: {}, enumerable: true, writable: true });
|
|
598
|
-
observations[owner][formatActivityId(
|
|
576
|
+
observations[owner][formatActivityId(integer(row.act_index))] = { state: 'seen', at: finite(row.at) };
|
|
599
577
|
}
|
|
600
|
-
for (const row of db.prepare(
|
|
601
|
-
|
|
602
|
-
const selector = object(json(row.selector_json));
|
|
603
|
-
Object.defineProperty(leases, owner, { value: validateWatchLease({
|
|
604
|
-
leaseId: decoded(row.claim_id_json, true), heartbeatAt: finite(row.heartbeat_at), expiresAt: finite(row.expires_at), ...(selector.filter === undefined ? {} : { filter: selector.filter })
|
|
605
|
-
}), enumerable: true });
|
|
578
|
+
for (const row of db.prepare('SELECT p.name_json,l.lease_json FROM catch_leases l JOIN participants p ON p.name_key_json=l.recipient_key_json').all()) {
|
|
579
|
+
Object.defineProperty(leases, decoded(row.name_json, true), { value: validateWatchLease(json(row.lease_json)), enumerable: true });
|
|
606
580
|
}
|
|
607
581
|
return {
|
|
608
582
|
hardCap: h.hardCap, ...(h.throttlePerMinute === undefined ? {} : { throttlePerMinute: h.throttlePerMinute }), preamble: [...h.preamble], warmup: [...h.warmup],
|
|
609
|
-
acts: db.prepare('SELECT
|
|
583
|
+
acts: db.prepare('SELECT * FROM activities ORDER BY act_index').all().map(activityWithContent),
|
|
610
584
|
runtime: { nextActIndex: h.nextActIndex, observations, leases }
|
|
611
585
|
};
|
|
612
586
|
}
|
|
613
|
-
/** Read-only integrity inspection
|
|
614
|
-
export function inspectSqlite(db
|
|
615
|
-
return inspectRows(db, endpoint, lower);
|
|
616
|
-
}
|
|
617
|
-
function inspectRows(db, inspectEndpoint, lower) {
|
|
587
|
+
/** Read-only integrity inspection. */
|
|
588
|
+
export function inspectSqlite(db) {
|
|
618
589
|
const check = db.prepare('PRAGMA integrity_check').all();
|
|
619
590
|
requireFact(check.length === 1 && check[0].integrity_check === 'ok', 'SQLite integrity check failed.');
|
|
620
591
|
const state = exportSqliteState(db), d = createSqliteDriver(db);
|
|
621
592
|
d.roster();
|
|
622
|
-
for (const row of db.prepare('SELECT * FROM endpoints').all())
|
|
623
|
-
inspectEndpoint(row);
|
|
624
|
-
for (const row of db.prepare('SELECT * FROM delivery_evidence').all())
|
|
625
|
-
deliveryEvidence(row);
|
|
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);
|
|
633
|
-
}
|
|
634
593
|
for (const p of d.roster()) {
|
|
635
594
|
d.attention(p.name);
|
|
636
595
|
d.incomingAttention(p.name);
|
|
@@ -638,40 +597,5 @@ function inspectRows(db, inspectEndpoint, lower) {
|
|
|
638
597
|
for (const a of state.acts)
|
|
639
598
|
if (a.kind === 'say')
|
|
640
599
|
d.audience(a.index);
|
|
641
|
-
|
|
642
|
-
if (lower === undefined)
|
|
643
|
-
return { standingCount };
|
|
644
|
-
const rebuilt = new MemoryDatabase(':memory:');
|
|
645
|
-
try {
|
|
646
|
-
initializeSqlite(rebuilt, state, d.header().revision, lower);
|
|
647
|
-
const retainName = rebuilt.prepare('UPDATE participants SET name_json=? WHERE name_key_json=?');
|
|
648
|
-
for (const p of d.roster())
|
|
649
|
-
retainName.run(encode(p.name), key(p.name));
|
|
650
|
-
for (const table of ['participants', 'attention', 'landed_audience']) {
|
|
651
|
-
const original = db.prepare(`SELECT * FROM ${table} ORDER BY 1,2`).all();
|
|
652
|
-
const expected = rebuilt.prepare(`SELECT * FROM ${table} ORDER BY 1,2`).all();
|
|
653
|
-
requireFact(stableJson(original) === stableJson(expected), `derived ${table} index is inconsistent.`);
|
|
654
|
-
}
|
|
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 };
|
|
673
|
-
}
|
|
674
|
-
finally {
|
|
675
|
-
rebuilt.close();
|
|
676
|
-
}
|
|
600
|
+
return { standingCount: d.roster().filter(p => p.joined).length };
|
|
677
601
|
}
|