@vercel/board 0.0.0-stage → 0.15.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/README.md +179 -2
- package/dist/a2-5EG8FkZ8.d.mts +3359 -0
- package/dist/a2-DYGYankn.mjs +3045 -0
- package/dist/access-CJU2-vaa.mjs +226 -0
- package/dist/access-CV96SYgr.d.mts +32 -0
- package/dist/access.d.mts +29 -0
- package/dist/access.mjs +4 -0
- package/dist/actors-rdN4dDTU.mjs +223 -0
- package/dist/attempts-BQUp9MKO.mjs +919 -0
- package/dist/auth-COzc09sn.mjs +276 -0
- package/dist/auth-wHFcAte3.d.mts +203 -0
- package/dist/backend-a2/blob-vercel.d.mts +8 -0
- package/dist/backend-a2/blob-vercel.mjs +81 -0
- package/dist/backend-a2/contract.d.mts +18 -0
- package/dist/backend-a2/contract.mjs +5 -0
- package/dist/backend-a2/scheduler-local.d.mts +11 -0
- package/dist/backend-a2/scheduler-local.mjs +2 -0
- package/dist/backend-a2/scheduler-qstash.d.mts +2 -0
- package/dist/backend-a2/scheduler-qstash.mjs +2 -0
- package/dist/backend-a2/scheduler-vercel.d.mts +2 -0
- package/dist/backend-a2/scheduler-vercel.mjs +2 -0
- package/dist/backend-a2/store-memory.d.mts +11 -0
- package/dist/backend-a2/store-memory.mjs +19 -0
- package/dist/backend-a2/store-postgres.d.mts +38 -0
- package/dist/backend-a2/store-postgres.mjs +110 -0
- package/dist/backend-a2/store-redis-http.d.mts +17 -0
- package/dist/backend-a2/store-redis-http.mjs +32 -0
- package/dist/backend-a2/store-redis.d.mts +23 -0
- package/dist/backend-a2/store-redis.mjs +39 -0
- package/dist/backend-a2/store-sqlite.d.mts +12 -0
- package/dist/backend-a2/store-sqlite.mjs +31 -0
- package/dist/backend-a2.d.mts +4 -0
- package/dist/backend-a2.mjs +2 -0
- package/dist/backend-http-8ex5g7Wv.d.mts +630 -0
- package/dist/backend-http-BsDSgmeo.mjs +613 -0
- package/dist/backend-http-Cv6xbEy9.mjs +469 -0
- package/dist/backend-http.d.mts +10 -0
- package/dist/backend-http.mjs +2 -0
- package/dist/backend-http.node.d.mts +10 -0
- package/dist/backend-http.node.mjs +2 -0
- package/dist/blob-C0J3wbGv.d.mts +39 -0
- package/dist/bots-CGkO76qH.mjs +67 -0
- package/dist/chat-provider-XCnQlxq6.d.mts +140 -0
- package/dist/chat-provider-jFSeyNmB.mjs +389 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.mjs +2252 -0
- package/dist/client-DGDPaN7U.mjs +380 -0
- package/dist/conformance-DRR23YNy.mjs +512 -0
- package/dist/conformance.d.mts +30 -0
- package/dist/conformance.mjs +2 -0
- package/dist/contract-CuJmlrhw.mjs +566 -0
- package/dist/emoji-CTLpvH-A.mjs +445 -0
- package/dist/engine-3EvQ8idk.mjs +1449 -0
- package/dist/errors-DX3hpwWt.mjs +64 -0
- package/dist/exports-BpgyEdh6.d.mts +4 -0
- package/dist/exports-CyY3_FjZ.mjs +1285 -0
- package/dist/files-SniSmeEr.mjs +47 -0
- package/dist/index.d.mts +13 -0
- package/dist/index.default-BZ3lItjH.mjs +12 -0
- package/dist/index.mjs +19 -0
- package/dist/index.node.d.mts +14 -0
- package/dist/index.node.mjs +5 -0
- package/dist/inspect/app.js +416 -0
- package/dist/inspect/engine.js +5 -0
- package/dist/local-Cr_dSjvs.mjs +44 -0
- package/dist/own-OwgPWdmh.mjs +15 -0
- package/dist/project-CFiArraX.mjs +124 -0
- package/dist/provider-BGjz-mDa.d.mts +2473 -0
- package/dist/provider-CuewcDNZ.mjs +205 -0
- package/dist/redis-tables-B35vv4CZ.mjs +842 -0
- package/dist/reducers-B_vUGQRp.d.mts +1172 -0
- package/dist/reducers-mUPFuFJV.mjs +293 -0
- package/dist/schemas-Be08nyfW.mjs +74 -0
- package/dist/schemas-CWxs3ECJ.d.mts +50 -0
- package/dist/serve-Ccbb2LSb.mjs +170 -0
- package/dist/server-Cxey38B-.mjs +17 -0
- package/dist/serving-BNbzc96L.mjs +2 -0
- package/dist/sql-DbiEnqM0.mjs +571 -0
- package/dist/sqlite-db-Cbj03vmp.mjs +70 -0
- package/dist/sync-github.d.mts +473 -0
- package/dist/sync-github.mjs +603 -0
- package/dist/sync-kapso.d.mts +175 -0
- package/dist/sync-kapso.mjs +86 -0
- package/dist/sync-slack.d.mts +364 -0
- package/dist/sync-slack.mjs +728 -0
- package/dist/sync.chat.d.mts +2 -0
- package/dist/sync.chat.mjs +2 -0
- package/dist/sync.d.mts +3 -0
- package/dist/sync.mjs +4 -0
- package/dist/targets-ojFEXEyN.mjs +94 -0
- package/dist/tokens-DKLn5P9B.mjs +256 -0
- package/dist/types-BHCjTZdB.d.mts +104 -0
- package/dist/users-Dls7_ANV.mjs +42 -0
- package/dist/watch-spec-W0c7ypHJ.mjs +787 -0
- package/dist/watchers-hash-D9hbHLKB.mjs +225 -0
- package/docs/01-board/01-mounting.md +506 -0
- package/docs/01-board/02-database.md +258 -0
- package/docs/01-board/03-identity.md +262 -0
- package/docs/01-board/04-messages.md +192 -0
- package/docs/01-board/05-reading.md +177 -0
- package/docs/01-board/06-watchers.md +249 -0
- package/docs/01-board/07-schedules.md +133 -0
- package/docs/01-board/08-middleware.md +139 -0
- package/docs/01-board/09-access.md +499 -0
- package/docs/01-quickstart.md +196 -0
- package/docs/02-sync/01-overview.md +290 -0
- package/docs/02-sync/02-callbacks.md +265 -0
- package/docs/02-sync/03-slack.md +248 -0
- package/docs/02-sync/04-github.md +201 -0
- package/docs/02-sync/05-whatsapp.md +148 -0
- package/docs/02-sync/06-providers.md +228 -0
- package/docs/03-guides/01-coding-agents.md +226 -0
- package/docs/03-guides/02-inspector.md +104 -0
- package/docs/03-guides/03-ci.md +251 -0
- package/docs/03-guides/04-triage.md +233 -0
- package/docs/03-guides/05-agent-factory.md +142 -0
- package/docs/04-reference/01-cli.md +606 -0
- package/docs/04-reference/02-board.md +579 -0
- package/docs/04-reference/03-http.md +489 -0
- package/docs/04-reference/04-log.md +250 -0
- package/docs/04-reference/05-conformance.md +97 -0
- package/docs/index.md +186 -0
- package/package.json +165 -3
- package/sql/postgres/board.sql +145 -0
- package/sql/postgres/notifications.sql +24 -0
- package/sql/postgres/schema.sql +451 -0
|
@@ -0,0 +1,571 @@
|
|
|
1
|
+
import { n as setOwn } from "./own-OwgPWdmh.mjs";
|
|
2
|
+
import { r as externalMessageKey } from "./reducers-mUPFuFJV.mjs";
|
|
3
|
+
import { i as syncReads, n as project, r as projectSync, t as concernedMessageId } from "./project-CFiArraX.mjs";
|
|
4
|
+
//#region src/store/sql.ts
|
|
5
|
+
/** The projection's name in `board_positions`, and the A2 consumer that runs it. */
|
|
6
|
+
const TABLES_CONSUMER = "board-tables";
|
|
7
|
+
/** The tables, as statements for one dialect. Postgres users apply them from `schema.sql`. */
|
|
8
|
+
function ddl(dialect) {
|
|
9
|
+
const big = dialect === "postgres" ? "bigint" : "integer";
|
|
10
|
+
const bool = dialect === "postgres" ? "boolean" : "integer";
|
|
11
|
+
return [
|
|
12
|
+
`create table if not exists board_positions (
|
|
13
|
+
consumer text primary key,
|
|
14
|
+
log_index ${big} not null
|
|
15
|
+
)`,
|
|
16
|
+
`create table if not exists board_messages (
|
|
17
|
+
id ${big} primary key,
|
|
18
|
+
event_id text not null unique,
|
|
19
|
+
channel text not null,
|
|
20
|
+
re ${big},
|
|
21
|
+
by_actor text not null,
|
|
22
|
+
by_principal text not null,
|
|
23
|
+
by_profile text,
|
|
24
|
+
from_key text,
|
|
25
|
+
no_sync ${bool} not null,
|
|
26
|
+
text_lower text not null,
|
|
27
|
+
deleted ${bool} not null,
|
|
28
|
+
created_at ${big} not null,
|
|
29
|
+
replies integer not null default 0,
|
|
30
|
+
at ${big},
|
|
31
|
+
data text not null
|
|
32
|
+
)`,
|
|
33
|
+
"create index if not exists board_messages_channel on board_messages (channel, id)",
|
|
34
|
+
"create index if not exists board_messages_re on board_messages (re, id)",
|
|
35
|
+
"create index if not exists board_messages_by_actor on board_messages (by_actor, id)",
|
|
36
|
+
"create index if not exists board_messages_by_principal on board_messages (by_principal, id)",
|
|
37
|
+
"create index if not exists board_threads_at on board_messages (at) where re is null",
|
|
38
|
+
"create index if not exists board_threads_channel on board_messages (channel, at) where re is null",
|
|
39
|
+
`create table if not exists board_versions (
|
|
40
|
+
log_index ${big} primary key,
|
|
41
|
+
message_id ${big} not null,
|
|
42
|
+
data text not null
|
|
43
|
+
)`,
|
|
44
|
+
"create index if not exists board_versions_message on board_versions (message_id, log_index)",
|
|
45
|
+
`create table if not exists board_terms (
|
|
46
|
+
message_id ${big} not null,
|
|
47
|
+
term text not null,
|
|
48
|
+
value text not null,
|
|
49
|
+
primary key (message_id, term, value)
|
|
50
|
+
)`,
|
|
51
|
+
"create index if not exists board_terms_term on board_terms (term, value, message_id)",
|
|
52
|
+
"create table if not exists board_sync_threads (\n thread text primary key,\n data text not null\n)",
|
|
53
|
+
"create table if not exists board_external_threads (\n external text primary key,\n thread text not null\n)",
|
|
54
|
+
`create table if not exists board_copies (
|
|
55
|
+
message_id ${big} not null,
|
|
56
|
+
target text not null,
|
|
57
|
+
data text not null,
|
|
58
|
+
primary key (message_id, target)
|
|
59
|
+
)`,
|
|
60
|
+
`create table if not exists board_external_messages (
|
|
61
|
+
key text primary key,
|
|
62
|
+
message_id ${big} not null
|
|
63
|
+
)`,
|
|
64
|
+
"create index if not exists board_messages_recent on board_messages (by_principal, created_at) where re is null",
|
|
65
|
+
...dialect === "postgres" ? ["drop function if exists board_apply(text, bigint, boolean, jsonb, jsonb, jsonb)", APPLY_FUNCTION] : []
|
|
66
|
+
];
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Postgres applies an event's change in one call, one round trip: claim the
|
|
70
|
+
* position, then write the message, its version, its terms, and its root's
|
|
71
|
+
* summary. `apply_next` claims only the very next index. SQLite runs the same
|
|
72
|
+
* writes as statements, in process.
|
|
73
|
+
*/
|
|
74
|
+
const APPLY_FUNCTION = `create or replace function board_apply(
|
|
75
|
+
apply_consumer text,
|
|
76
|
+
apply_index bigint,
|
|
77
|
+
apply_next boolean,
|
|
78
|
+
apply_message jsonb,
|
|
79
|
+
apply_terms jsonb,
|
|
80
|
+
apply_summary jsonb,
|
|
81
|
+
apply_sync jsonb
|
|
82
|
+
) returns boolean language plpgsql as $$
|
|
83
|
+
begin
|
|
84
|
+
if apply_next then
|
|
85
|
+
update board_positions set log_index = apply_index
|
|
86
|
+
where consumer = apply_consumer and log_index = apply_index - 1;
|
|
87
|
+
else
|
|
88
|
+
insert into board_positions (consumer, log_index) values (apply_consumer, apply_index)
|
|
89
|
+
on conflict (consumer) do update set log_index = excluded.log_index
|
|
90
|
+
where board_positions.log_index < excluded.log_index;
|
|
91
|
+
end if;
|
|
92
|
+
if not found then
|
|
93
|
+
return false;
|
|
94
|
+
end if;
|
|
95
|
+
if apply_message is not null then
|
|
96
|
+
if (apply_message->>'created')::boolean then
|
|
97
|
+
insert into board_messages
|
|
98
|
+
(id, event_id, channel, re, by_actor, by_principal, by_profile, from_key, no_sync, text_lower, deleted, created_at, data)
|
|
99
|
+
values (
|
|
100
|
+
(apply_message->>'id')::bigint, apply_message->>'event_id', apply_message->>'channel', (apply_message->>'re')::bigint,
|
|
101
|
+
apply_message->>'by_actor', apply_message->>'by_principal', apply_message->>'by_profile', apply_message->>'from_key',
|
|
102
|
+
(apply_message->>'no_sync')::boolean, apply_message->>'text_lower', (apply_message->>'deleted')::boolean,
|
|
103
|
+
(apply_message->>'created_at')::bigint, apply_message->>'data');
|
|
104
|
+
else
|
|
105
|
+
update board_messages
|
|
106
|
+
set text_lower = apply_message->>'text_lower', deleted = (apply_message->>'deleted')::boolean, data = apply_message->>'data'
|
|
107
|
+
where id = (apply_message->>'id')::bigint;
|
|
108
|
+
end if;
|
|
109
|
+
insert into board_versions (log_index, message_id, data)
|
|
110
|
+
values (apply_index, (apply_message->>'id')::bigint, apply_message->>'data');
|
|
111
|
+
if apply_terms is not null then
|
|
112
|
+
delete from board_terms where message_id = (apply_message->>'id')::bigint;
|
|
113
|
+
insert into board_terms (message_id, term, value)
|
|
114
|
+
select (apply_message->>'id')::bigint, term->>0, term->>1 from jsonb_array_elements(apply_terms) as term;
|
|
115
|
+
end if;
|
|
116
|
+
end if;
|
|
117
|
+
if apply_summary is not null then
|
|
118
|
+
update board_messages set replies = (apply_summary->>'replies')::int, at = (apply_summary->>'at')::bigint
|
|
119
|
+
where id = (apply_summary->>'root')::bigint;
|
|
120
|
+
end if;
|
|
121
|
+
if apply_sync is not null then
|
|
122
|
+
if jsonb_typeof(apply_sync->'thread') = 'object' then
|
|
123
|
+
insert into board_sync_threads (thread, data) values (apply_sync->'thread'->>'key', apply_sync->'thread'->>'data')
|
|
124
|
+
on conflict (thread) do update set data = excluded.data;
|
|
125
|
+
end if;
|
|
126
|
+
insert into board_external_threads (external, thread)
|
|
127
|
+
select entry->>0, entry->>1 from jsonb_array_elements(apply_sync->'external_threads') as entry
|
|
128
|
+
on conflict (external) do update set thread = excluded.thread;
|
|
129
|
+
insert into board_copies (message_id, target, data)
|
|
130
|
+
select (entry->>0)::bigint, entry->>1, entry->>2 from jsonb_array_elements(apply_sync->'copies') as entry
|
|
131
|
+
on conflict (message_id, target) do update set data = excluded.data;
|
|
132
|
+
insert into board_external_messages (key, message_id)
|
|
133
|
+
select entry->>0, (entry->>1)::bigint from jsonb_array_elements(apply_sync->'external_messages') as entry
|
|
134
|
+
on conflict (key) do update set message_id = excluded.message_id;
|
|
135
|
+
end if;
|
|
136
|
+
return true;
|
|
137
|
+
end
|
|
138
|
+
$$`;
|
|
139
|
+
/**
|
|
140
|
+
* Postgres refuses NUL in text, and a lone surrogate in JSON: the board refuses
|
|
141
|
+
* both, and a log from before that may hold them. Nothing a reader filters on
|
|
142
|
+
* needs either, so keys and columns go without NUL and with U+FFFD for a lone
|
|
143
|
+
* surrogate, and the lookups that find them do the same.
|
|
144
|
+
*/
|
|
145
|
+
const clean = (value) => value.replaceAll("\0", "").toWellFormed();
|
|
146
|
+
/** A mention's term. Label keys start with a letter or digit, so it's never a label's. */
|
|
147
|
+
const MENTION = "@";
|
|
148
|
+
/** An attachment's term, the same way. */
|
|
149
|
+
const ATTACHMENT = "^";
|
|
150
|
+
/** Terms one insert takes, three parameters each. */
|
|
151
|
+
const TERMS_PER_INSERT = 300;
|
|
152
|
+
var Params = class {
|
|
153
|
+
values = [];
|
|
154
|
+
add(value) {
|
|
155
|
+
this.values.push(value);
|
|
156
|
+
return `$${this.values.length}`;
|
|
157
|
+
}
|
|
158
|
+
list(values) {
|
|
159
|
+
return values.map((value) => this.add(value)).join(", ");
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
const likeEscape = (text) => text.replace(/[\\%_]/g, (c) => `\\${c}`);
|
|
163
|
+
/** A channel, or a channel and everything under it, on the messages table aliased `alias`. */
|
|
164
|
+
function channelClause(alias, channel, p) {
|
|
165
|
+
const path = p.add(channel.path);
|
|
166
|
+
if (!channel.subtree) return `${alias}.channel = ${path}`;
|
|
167
|
+
const prefix = `cast(${p.add(`${channel.path}/`)} as text)`;
|
|
168
|
+
return `(${alias}.channel = ${path} or substr(${alias}.channel, 1, length(${prefix})) = ${prefix})`;
|
|
169
|
+
}
|
|
170
|
+
/** The filter as `where` clauses on the messages table aliased `m`. */
|
|
171
|
+
function clauses(filter, p) {
|
|
172
|
+
const out = [];
|
|
173
|
+
if (filter.channel) out.push(channelClause("m", filter.channel, p));
|
|
174
|
+
if (filter.channels) out.push(filter.channels.length ? `m.channel in (${p.list(filter.channels)})` : "1 = 0");
|
|
175
|
+
if (filter.re !== void 0) {
|
|
176
|
+
const re = p.add(filter.re);
|
|
177
|
+
out.push(`(m.id = ${re} or m.re = ${re})`);
|
|
178
|
+
}
|
|
179
|
+
if (filter.by) {
|
|
180
|
+
const { actors, principals } = filter.by;
|
|
181
|
+
const either = [...actors.length ? [`m.by_actor in (${p.list(actors)})`] : [], ...principals.length ? [`m.by_principal in (${p.list(principals)})`] : []];
|
|
182
|
+
out.push(either.length ? `(${either.join(" or ")})` : "1 = 0");
|
|
183
|
+
}
|
|
184
|
+
if (filter.mentions) out.push(filter.mentions.length ? `exists (select 1 from board_terms t where t.message_id = m.id and t.term = '${MENTION}' and t.value in (${p.list(filter.mentions)}))` : "1 = 0");
|
|
185
|
+
if (filter.from !== void 0) out.push(`coalesce(m.from_key, 'board') = ${p.add(filter.from)}`);
|
|
186
|
+
if (filter.text !== void 0) out.push(`m.text_lower like ${p.add(`%${likeEscape(filter.text)}%`)} escape '\\'`);
|
|
187
|
+
for (const label of filter.labels ?? []) {
|
|
188
|
+
const values = label.values === null ? "" : ` and t.value in (${p.list(label.values)})`;
|
|
189
|
+
out.push(`exists (select 1 from board_terms t where t.message_id in (m.id, m.re) and t.term = ${p.add(label.key)}${values})`);
|
|
190
|
+
}
|
|
191
|
+
return out;
|
|
192
|
+
}
|
|
193
|
+
const parse = (row) => row === void 0 || row.data === null || row.data === void 0 ? null : JSON.parse(String(row.data));
|
|
194
|
+
/** The tables on a SQL database. */
|
|
195
|
+
function sqlTables(db) {
|
|
196
|
+
const positionSql = {
|
|
197
|
+
text: "select log_index from board_positions where consumer = $1",
|
|
198
|
+
values: [TABLES_CONSUMER]
|
|
199
|
+
};
|
|
200
|
+
const position = (rows) => rows[0] ? Number(rows[0].log_index) : 0;
|
|
201
|
+
const claim = (index) => ({
|
|
202
|
+
text: `insert into board_positions (consumer, log_index) values ($1, $2)
|
|
203
|
+
on conflict (consumer) do update set log_index = excluded.log_index
|
|
204
|
+
where board_positions.log_index < excluded.log_index
|
|
205
|
+
returning 1 as claimed`,
|
|
206
|
+
values: [TABLES_CONSUMER, index]
|
|
207
|
+
});
|
|
208
|
+
/** Only the event right after the position: never ahead of events the tables haven't applied. */
|
|
209
|
+
const claimNext = (index) => ({
|
|
210
|
+
text: "update board_positions set log_index = $2 where consumer = $1 and log_index = $3 returning 1 as claimed",
|
|
211
|
+
values: [
|
|
212
|
+
TABLES_CONSUMER,
|
|
213
|
+
index,
|
|
214
|
+
index - 1
|
|
215
|
+
]
|
|
216
|
+
});
|
|
217
|
+
async function row(id) {
|
|
218
|
+
const [found] = await db.all({
|
|
219
|
+
text: "select data, replies, at from board_messages where id = $1",
|
|
220
|
+
values: [id]
|
|
221
|
+
});
|
|
222
|
+
return found;
|
|
223
|
+
}
|
|
224
|
+
async function before(event, id) {
|
|
225
|
+
const found = event.type === "message.posted" ? void 0 : await row(id);
|
|
226
|
+
const message = parse(found) ?? void 0;
|
|
227
|
+
const rootId = event.type === "message.posted" ? event.payload.re : message?.re ?? null;
|
|
228
|
+
if (rootId === null) return {
|
|
229
|
+
message,
|
|
230
|
+
root: void 0
|
|
231
|
+
};
|
|
232
|
+
const rootRow = await row(rootId);
|
|
233
|
+
const root = rootRow ? {
|
|
234
|
+
replies: Number(rootRow.replies),
|
|
235
|
+
at: Number(rootRow.at)
|
|
236
|
+
} : void 0;
|
|
237
|
+
if (event.type === "message.deleted" && message && !message.deleted && root?.at === id) {
|
|
238
|
+
const [newest] = await db.all({
|
|
239
|
+
text: "select max(id) as id from board_messages where re = $1 and not deleted and id <> $2",
|
|
240
|
+
values: [rootId, id]
|
|
241
|
+
});
|
|
242
|
+
return {
|
|
243
|
+
message,
|
|
244
|
+
root,
|
|
245
|
+
previousLast: newest?.id === null || newest?.id === void 0 ? null : Number(newest.id)
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
return {
|
|
249
|
+
message,
|
|
250
|
+
root
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
async function syncBefore(event) {
|
|
254
|
+
const reads = syncReads(event);
|
|
255
|
+
const [thread] = reads.thread === null ? [] : await db.all({
|
|
256
|
+
text: "select data from board_sync_threads where thread = $1",
|
|
257
|
+
values: [clean(reads.thread)]
|
|
258
|
+
});
|
|
259
|
+
const [copy] = reads.copy === null ? [] : await db.all({
|
|
260
|
+
text: "select data from board_copies where message_id = $1 and target = $2",
|
|
261
|
+
values: [reads.copy[0], clean(reads.copy[1])]
|
|
262
|
+
});
|
|
263
|
+
return {
|
|
264
|
+
thread: thread ? JSON.parse(String(thread.data)) : void 0,
|
|
265
|
+
copy: copy ? JSON.parse(String(copy.data)) : void 0
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
/** The sync rows an event writes, as values; null when it writes none. */
|
|
269
|
+
function syncPayload(change) {
|
|
270
|
+
if (!change.thread && !change.externalThreads.length && !change.copies.length && !change.externalMessages.length) return null;
|
|
271
|
+
return {
|
|
272
|
+
thread: change.thread ? {
|
|
273
|
+
key: clean(change.thread.key),
|
|
274
|
+
data: JSON.stringify(change.thread.data)
|
|
275
|
+
} : null,
|
|
276
|
+
external_threads: change.externalThreads.map(([external, thread]) => [clean(external), clean(thread)]),
|
|
277
|
+
copies: change.copies.map(([message, target, copy]) => [
|
|
278
|
+
message,
|
|
279
|
+
clean(target),
|
|
280
|
+
JSON.stringify(copy)
|
|
281
|
+
]),
|
|
282
|
+
external_messages: change.externalMessages.map(([key, message]) => [clean(key), message])
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
/** What an event writes, as column values: the one place they're derived. */
|
|
286
|
+
function payload(event, change, sync) {
|
|
287
|
+
const m = change.message?.after;
|
|
288
|
+
return {
|
|
289
|
+
message: m && {
|
|
290
|
+
id: m.id,
|
|
291
|
+
created: change.message.created,
|
|
292
|
+
event_id: clean(event.id),
|
|
293
|
+
channel: m.channel,
|
|
294
|
+
re: m.re,
|
|
295
|
+
by_actor: clean(m.by.actor),
|
|
296
|
+
by_principal: clean(m.by.principal),
|
|
297
|
+
by_profile: m.by.profile === null ? null : clean(m.by.profile),
|
|
298
|
+
from_key: m.from,
|
|
299
|
+
no_sync: m.noSync,
|
|
300
|
+
text_lower: clean(m.text.toLowerCase()),
|
|
301
|
+
deleted: m.deleted,
|
|
302
|
+
created_at: Date.parse(m.createdAt),
|
|
303
|
+
data: JSON.stringify(m)
|
|
304
|
+
},
|
|
305
|
+
terms: m && change.message.terms ? [
|
|
306
|
+
...Object.entries(m.labels).map(([key, value]) => [key, clean(value)]),
|
|
307
|
+
...[...new Set(m.mentions)].map((actor) => [MENTION, clean(actor)]),
|
|
308
|
+
...[...new Set(m.attachments.map((file) => file.id))].map((id) => [ATTACHMENT, clean(id)])
|
|
309
|
+
] : null,
|
|
310
|
+
summary: change.summary ?? null,
|
|
311
|
+
sync: syncPayload(sync)
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
/** The writes as statements, for SQLite. Postgres's `board_apply` does the same. */
|
|
315
|
+
function statements(index, { message: m, terms, summary, sync }) {
|
|
316
|
+
const out = [];
|
|
317
|
+
if (m) {
|
|
318
|
+
if (m.created) out.push({
|
|
319
|
+
text: `insert into board_messages
|
|
320
|
+
(id, event_id, channel, re, by_actor, by_principal, by_profile, from_key, no_sync, text_lower, deleted, created_at, data)
|
|
321
|
+
values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)`,
|
|
322
|
+
values: [
|
|
323
|
+
m.id,
|
|
324
|
+
m.event_id,
|
|
325
|
+
m.channel,
|
|
326
|
+
m.re,
|
|
327
|
+
m.by_actor,
|
|
328
|
+
m.by_principal,
|
|
329
|
+
m.by_profile,
|
|
330
|
+
m.from_key,
|
|
331
|
+
m.no_sync,
|
|
332
|
+
m.text_lower,
|
|
333
|
+
m.deleted,
|
|
334
|
+
m.created_at,
|
|
335
|
+
m.data
|
|
336
|
+
]
|
|
337
|
+
});
|
|
338
|
+
else out.push({
|
|
339
|
+
text: "update board_messages set text_lower = $1, deleted = $2, data = $3 where id = $4",
|
|
340
|
+
values: [
|
|
341
|
+
m.text_lower,
|
|
342
|
+
m.deleted,
|
|
343
|
+
m.data,
|
|
344
|
+
m.id
|
|
345
|
+
]
|
|
346
|
+
});
|
|
347
|
+
out.push({
|
|
348
|
+
text: "insert into board_versions (log_index, message_id, data) values ($1, $2, $3)",
|
|
349
|
+
values: [
|
|
350
|
+
index,
|
|
351
|
+
m.id,
|
|
352
|
+
m.data
|
|
353
|
+
]
|
|
354
|
+
});
|
|
355
|
+
if (terms) {
|
|
356
|
+
if (!m.created) out.push({
|
|
357
|
+
text: "delete from board_terms where message_id = $1",
|
|
358
|
+
values: [m.id]
|
|
359
|
+
});
|
|
360
|
+
for (let first = 0; first < terms.length; first += TERMS_PER_INSERT) {
|
|
361
|
+
const p = new Params();
|
|
362
|
+
const tuples = terms.slice(first, first + TERMS_PER_INSERT).map(([term, value]) => `(${p.add(m.id)}, ${p.add(term)}, ${p.add(value)})`);
|
|
363
|
+
out.push({
|
|
364
|
+
text: `insert into board_terms (message_id, term, value) values ${tuples.join(", ")}`,
|
|
365
|
+
values: p.values
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
if (summary) out.push({
|
|
371
|
+
text: "update board_messages set replies = $1, at = $2 where id = $3",
|
|
372
|
+
values: [
|
|
373
|
+
summary.replies,
|
|
374
|
+
summary.at,
|
|
375
|
+
summary.root
|
|
376
|
+
]
|
|
377
|
+
});
|
|
378
|
+
if (sync) {
|
|
379
|
+
if (sync.thread) out.push({
|
|
380
|
+
text: "insert into board_sync_threads (thread, data) values ($1, $2) on conflict (thread) do update set data = excluded.data",
|
|
381
|
+
values: [sync.thread.key, sync.thread.data]
|
|
382
|
+
});
|
|
383
|
+
for (const [external, thread] of sync.external_threads) out.push({
|
|
384
|
+
text: "insert into board_external_threads (external, thread) values ($1, $2) on conflict (external) do update set thread = excluded.thread",
|
|
385
|
+
values: [external, thread]
|
|
386
|
+
});
|
|
387
|
+
for (const [message, target, data] of sync.copies) out.push({
|
|
388
|
+
text: "insert into board_copies (message_id, target, data) values ($1, $2, $3) on conflict (message_id, target) do update set data = excluded.data",
|
|
389
|
+
values: [
|
|
390
|
+
message,
|
|
391
|
+
target,
|
|
392
|
+
data
|
|
393
|
+
]
|
|
394
|
+
});
|
|
395
|
+
for (const [key, message] of sync.external_messages) out.push({
|
|
396
|
+
text: "insert into board_external_messages (key, message_id) values ($1, $2) on conflict (key) do update set message_id = excluded.message_id",
|
|
397
|
+
values: [key, message]
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
return out;
|
|
401
|
+
}
|
|
402
|
+
/** Claims `event` (only if it's the next one, with `next`) and writes what it changes, atomically. */
|
|
403
|
+
async function commit(event, next) {
|
|
404
|
+
if (next && position(await db.all(positionSql)) !== event.index - 1) return false;
|
|
405
|
+
const id = concernedMessageId(event);
|
|
406
|
+
const [change, sync] = await Promise.all([id === null ? {} : before(event, id).then((read) => project(read, event)), syncBefore(event).then((read) => projectSync(read, event))]);
|
|
407
|
+
const written = payload(event, change, sync);
|
|
408
|
+
if (db.dialect === "postgres") {
|
|
409
|
+
const json = (value) => value === null || value === void 0 ? null : JSON.stringify(value);
|
|
410
|
+
const [row] = await db.all({
|
|
411
|
+
text: "select board_apply($1, $2, $3, $4::jsonb, $5::jsonb, $6::jsonb, $7::jsonb) as applied",
|
|
412
|
+
values: [
|
|
413
|
+
TABLES_CONSUMER,
|
|
414
|
+
event.index,
|
|
415
|
+
next,
|
|
416
|
+
json(written.message),
|
|
417
|
+
json(written.terms),
|
|
418
|
+
json(written.summary),
|
|
419
|
+
json(written.sync)
|
|
420
|
+
]
|
|
421
|
+
});
|
|
422
|
+
return row?.applied === true;
|
|
423
|
+
}
|
|
424
|
+
return db.commit(next ? claimNext(event.index) : claim(event.index), statements(event.index, written));
|
|
425
|
+
}
|
|
426
|
+
function threadsQuery(filter, page) {
|
|
427
|
+
const p = new Params();
|
|
428
|
+
const where = ["r.re is null"];
|
|
429
|
+
const { channel, channels, ...rest } = filter;
|
|
430
|
+
if (channel) where.push(channelClause("r", channel, p));
|
|
431
|
+
if (channels) where.push(channels.length ? `r.channel in (${p.list(channels)})` : "1 = 0");
|
|
432
|
+
const matching = clauses(rest, p);
|
|
433
|
+
if (matching.length > 0) where.push(`r.id in (select coalesce(m.re, m.id) from board_messages m where ${matching.join(" and ")})`);
|
|
434
|
+
if (page.after !== void 0) where.push(`r.at > ${p.add(page.after)}`);
|
|
435
|
+
const order = page.after !== void 0 ? "asc" : "desc";
|
|
436
|
+
return {
|
|
437
|
+
text: `select r.data as data, r.replies as replies, l.data as last
|
|
438
|
+
from board_messages r left join board_messages l on l.id = r.at and r.at <> r.id
|
|
439
|
+
where ${where.join(" and ")}
|
|
440
|
+
order by r.at ${order} limit ${p.add(page.limit)}`,
|
|
441
|
+
values: p.values
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
return {
|
|
445
|
+
async position() {
|
|
446
|
+
return position(await db.all(positionSql));
|
|
447
|
+
},
|
|
448
|
+
async apply(event) {
|
|
449
|
+
if (position(await db.all(positionSql)) >= event.index) return;
|
|
450
|
+
await commit(event, false);
|
|
451
|
+
},
|
|
452
|
+
applyNext: (event) => commit(event, true),
|
|
453
|
+
async message(id) {
|
|
454
|
+
return parse(await row(id));
|
|
455
|
+
},
|
|
456
|
+
async byEventId(eventId) {
|
|
457
|
+
const [found] = await db.all({
|
|
458
|
+
text: "select data from board_messages where event_id = $1",
|
|
459
|
+
values: [clean(eventId)]
|
|
460
|
+
});
|
|
461
|
+
return parse(found);
|
|
462
|
+
},
|
|
463
|
+
async messagesAt(ids, index) {
|
|
464
|
+
const out = /* @__PURE__ */ new Map();
|
|
465
|
+
if (ids.length === 0) return out;
|
|
466
|
+
const p = new Params();
|
|
467
|
+
const rows = await db.all({
|
|
468
|
+
text: `select v.message_id as id, v.data as data from board_versions v
|
|
469
|
+
where v.message_id in (${p.list([...new Set(ids)])})
|
|
470
|
+
and v.log_index = (select max(w.log_index) from board_versions w where w.message_id = v.message_id and w.log_index <= ${p.add(index)})`,
|
|
471
|
+
values: p.values
|
|
472
|
+
});
|
|
473
|
+
for (const found of rows) out.set(Number(found.id), parse(found));
|
|
474
|
+
return out;
|
|
475
|
+
},
|
|
476
|
+
async thread(root) {
|
|
477
|
+
return (await db.all({
|
|
478
|
+
text: "select data from board_messages where id = $1 or re = $1 order by id",
|
|
479
|
+
values: [root]
|
|
480
|
+
})).map((found) => parse(found));
|
|
481
|
+
},
|
|
482
|
+
async messages(filter, page) {
|
|
483
|
+
const p = new Params();
|
|
484
|
+
const where = clauses(filter, p);
|
|
485
|
+
if (page.before !== void 0) where.push(`m.id < ${p.add(page.before)}`);
|
|
486
|
+
if (page.after !== void 0) where.push(`m.id > ${p.add(page.after)}`);
|
|
487
|
+
const order = page.after !== void 0 ? "asc" : "desc";
|
|
488
|
+
const query = {
|
|
489
|
+
text: `select m.data as data from board_messages m${where.length ? ` where ${where.join(" and ")}` : ""} order by m.id ${order} limit ${p.add(page.limit)}`,
|
|
490
|
+
values: p.values
|
|
491
|
+
};
|
|
492
|
+
const [positions, rows] = await db.read([positionSql, query]);
|
|
493
|
+
const messages = rows.map((found) => parse(found));
|
|
494
|
+
return {
|
|
495
|
+
messages: page.after !== void 0 ? messages : messages.reverse(),
|
|
496
|
+
position: position(positions)
|
|
497
|
+
};
|
|
498
|
+
},
|
|
499
|
+
async threads(filter, page) {
|
|
500
|
+
const [positions, rows] = await db.read([positionSql, threadsQuery(filter, page)]);
|
|
501
|
+
const threads = rows.map((found) => ({
|
|
502
|
+
root: parse(found),
|
|
503
|
+
replies: Number(found.replies),
|
|
504
|
+
last: found.last === null || found.last === void 0 ? null : JSON.parse(String(found.last))
|
|
505
|
+
}));
|
|
506
|
+
return {
|
|
507
|
+
threads: page.after !== void 0 ? threads : threads.reverse(),
|
|
508
|
+
position: position(positions)
|
|
509
|
+
};
|
|
510
|
+
},
|
|
511
|
+
async syncOf(thread) {
|
|
512
|
+
const [found] = await db.all({
|
|
513
|
+
text: "select data from board_sync_threads where thread = $1",
|
|
514
|
+
values: [clean(thread)]
|
|
515
|
+
});
|
|
516
|
+
return found ? JSON.parse(String(found.data)) : null;
|
|
517
|
+
},
|
|
518
|
+
async threadOf(external) {
|
|
519
|
+
const [found] = await db.all({
|
|
520
|
+
text: "select thread from board_external_threads where external = $1",
|
|
521
|
+
values: [clean(external)]
|
|
522
|
+
});
|
|
523
|
+
return found ? String(found.thread) : null;
|
|
524
|
+
},
|
|
525
|
+
async copiesOf(ids) {
|
|
526
|
+
const out = /* @__PURE__ */ new Map();
|
|
527
|
+
if (ids.length === 0) return out;
|
|
528
|
+
const p = new Params();
|
|
529
|
+
const rows = await db.all({
|
|
530
|
+
text: `select message_id, target, data from board_copies where message_id in (${p.list([...new Set(ids)])})`,
|
|
531
|
+
values: p.values
|
|
532
|
+
});
|
|
533
|
+
for (const found of rows) {
|
|
534
|
+
const copies = out.get(Number(found.message_id)) ?? {};
|
|
535
|
+
setOwn(copies, String(found.target), JSON.parse(String(found.data)));
|
|
536
|
+
out.set(Number(found.message_id), copies);
|
|
537
|
+
}
|
|
538
|
+
return out;
|
|
539
|
+
},
|
|
540
|
+
async messageOf(externalThread, platformMessage) {
|
|
541
|
+
const [found] = await db.all({
|
|
542
|
+
text: "select message_id from board_external_messages where key = $1",
|
|
543
|
+
values: [clean(externalMessageKey(externalThread, platformMessage))]
|
|
544
|
+
});
|
|
545
|
+
return found ? Number(found.message_id) : null;
|
|
546
|
+
},
|
|
547
|
+
async externalThreads() {
|
|
548
|
+
return (await db.all({
|
|
549
|
+
text: "select external from board_external_threads",
|
|
550
|
+
values: []
|
|
551
|
+
})).map((found) => String(found.external));
|
|
552
|
+
},
|
|
553
|
+
async withAttachment(attachment) {
|
|
554
|
+
return (await db.all({
|
|
555
|
+
text: `select m.data as data from board_messages m
|
|
556
|
+
where exists (select 1 from board_terms t where t.message_id = m.id and t.term = '${ATTACHMENT}' and t.value = $1)
|
|
557
|
+
order by m.id`,
|
|
558
|
+
values: [clean(attachment)]
|
|
559
|
+
})).map((found) => parse(found));
|
|
560
|
+
},
|
|
561
|
+
async recentRoots(principal, since) {
|
|
562
|
+
return (await db.all({
|
|
563
|
+
text: "select data from board_messages where by_principal = $1 and re is null and created_at >= $2 order by id",
|
|
564
|
+
values: [principal, since]
|
|
565
|
+
})).map((found) => parse(found));
|
|
566
|
+
},
|
|
567
|
+
close: () => db.close()
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
//#endregion
|
|
571
|
+
export { ddl as n, sqlTables as r, TABLES_CONSUMER as t };
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { n as ddl } from "./sql-DbiEnqM0.mjs";
|
|
2
|
+
import { mkdirSync } from "node:fs";
|
|
3
|
+
import { dirname, resolve } from "node:path";
|
|
4
|
+
//#region src/store/sqlite-db.ts
|
|
5
|
+
/** `$1` → `?1`: SQLite's numbered parameters, bound in order. */
|
|
6
|
+
const numbered = (text) => text.replace(/\$(\d+)/g, "?$1");
|
|
7
|
+
/** SQLite has no booleans. */
|
|
8
|
+
const bind = (value) => typeof value === "boolean" ? value ? 1 : 0 : value ?? null;
|
|
9
|
+
/**
|
|
10
|
+
* The tables on `node:sqlite`, at `path` (`:memory:` works). Every transaction
|
|
11
|
+
* runs synchronously, from `begin` to `commit`: A2's SQLite store, on another
|
|
12
|
+
* handle to the same file, blocks the thread while it waits for the lock, so
|
|
13
|
+
* a transaction of ours that yielded in between would wait on itself.
|
|
14
|
+
*/
|
|
15
|
+
function sqliteDb(path) {
|
|
16
|
+
const { DatabaseSync: Database } = process.getBuiltinModule("node:sqlite");
|
|
17
|
+
if (path !== ":memory:") mkdirSync(dirname(resolve(
|
|
18
|
+
/* turbopackIgnore: true */
|
|
19
|
+
path
|
|
20
|
+
)), { recursive: true });
|
|
21
|
+
const db = new Database(path);
|
|
22
|
+
db.exec("pragma busy_timeout = 5000");
|
|
23
|
+
for (const statement of ddl("sqlite")) db.exec(statement);
|
|
24
|
+
const statements = /* @__PURE__ */ new Map();
|
|
25
|
+
const run = (query) => {
|
|
26
|
+
let statement = statements.get(query.text);
|
|
27
|
+
if (!statement) statements.set(query.text, statement = db.prepare(numbered(query.text)));
|
|
28
|
+
return statement.all(...query.values.map(bind));
|
|
29
|
+
};
|
|
30
|
+
const rollback = () => {
|
|
31
|
+
try {
|
|
32
|
+
db.exec("rollback");
|
|
33
|
+
} catch {}
|
|
34
|
+
};
|
|
35
|
+
return {
|
|
36
|
+
dialect: "sqlite",
|
|
37
|
+
all: async (query) => run(query),
|
|
38
|
+
async read(queries) {
|
|
39
|
+
db.exec("begin");
|
|
40
|
+
try {
|
|
41
|
+
const rows = queries.map(run);
|
|
42
|
+
db.exec("commit");
|
|
43
|
+
return rows;
|
|
44
|
+
} catch (error) {
|
|
45
|
+
rollback();
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
async commit(claim, writes) {
|
|
50
|
+
db.exec("begin immediate");
|
|
51
|
+
try {
|
|
52
|
+
if (run(claim).length === 0) {
|
|
53
|
+
db.exec("rollback");
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
for (const write of writes) run(write);
|
|
57
|
+
db.exec("commit");
|
|
58
|
+
return true;
|
|
59
|
+
} catch (error) {
|
|
60
|
+
rollback();
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
async close() {
|
|
65
|
+
db.close();
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
//#endregion
|
|
70
|
+
export { sqliteDb as t };
|