@vercel/board 0.16.0 → 0.18.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 +27 -27
- package/dist/{a2-C41hD8SF.mjs → a2-ChRLRupa.mjs} +969 -25
- package/dist/access.d.mts +1 -1
- package/dist/access.mjs +3 -3
- package/dist/{actors-DjMeZpnU.mjs → actors-BOamkEuz.mjs} +3 -73
- package/dist/attempts-CLG840Oj.mjs +20 -0
- package/dist/{auth-COzc09sn.mjs → auth-Drg9FiC2.mjs} +225 -2
- package/dist/backend-B-SJ7p6p.d.mts +680 -0
- package/dist/backend-a2/contract.d.mts +873 -2
- package/dist/backend-a2/contract.mjs +2 -2
- package/dist/backend-a2/scheduler-local.d.mts +1 -1
- package/dist/backend-a2/scheduler-local.mjs +1 -1
- package/dist/backend-a2/scheduler-qstash.d.mts +78 -1
- package/dist/backend-a2/scheduler-qstash.mjs +521 -1
- package/dist/backend-a2/scheduler-vercel.d.mts +37 -1
- package/dist/backend-a2/scheduler-vercel.mjs +203 -1
- package/dist/backend-a2/store-memory.d.mts +2 -2
- package/dist/backend-a2/store-memory.mjs +3 -3
- package/dist/backend-a2/store-postgres.d.mts +1 -1
- package/dist/backend-a2/store-postgres.mjs +2850 -2
- package/dist/backend-a2/store-redis-http.d.mts +1 -1
- package/dist/backend-a2/store-redis-http.mjs +490 -2
- package/dist/backend-a2/store-redis.d.mts +10 -2
- package/dist/backend-a2/store-redis.mjs +51 -2
- package/dist/backend-a2/store-sqlite.d.mts +1 -1
- package/dist/backend-a2/store-sqlite.mjs +3 -3
- package/dist/backend-a2.d.mts +2706 -3
- package/dist/backend-a2.mjs +1 -1
- package/dist/{backend-http-Bid7RIpC.d.mts → backend-http-7cNb8nJI.d.mts} +10 -3
- package/dist/{backend-http-0roEmjl0.mjs → backend-http-CMlL3yq3.mjs} +2 -2
- package/dist/{backend-http-BVRno-ca.mjs → backend-http-Ddzd5kTS.mjs} +32 -3
- package/dist/backend-http.d.mts +1 -1
- package/dist/backend-http.mjs +1 -1
- package/dist/backend-http.node.d.mts +1 -1
- package/dist/backend-http.node.mjs +1 -1
- package/dist/{chat-provider-XCnQlxq6.d.mts → chat-provider-DNJQyajS.d.mts} +16 -4
- package/dist/{chat-provider-jFSeyNmB.mjs → chat-provider-YHy-WbrB.mjs} +15 -9
- package/dist/cli.mjs +30 -10
- package/dist/{client-DGDPaN7U.mjs → client-BA-J7usH.mjs} +10 -2
- package/dist/{conformance-DRR23YNy.mjs → conformance-CT5EeHeC.mjs} +5 -2
- package/dist/conformance.d.mts +1 -1
- package/dist/conformance.mjs +1 -1
- package/dist/{engine-Bir5s8ts.mjs → engine-D0oBUBEK.mjs} +249 -163
- package/dist/events-CewD4kw7.mjs +75 -0
- package/dist/{exports-Dvc1o52I.mjs → exports-CgjfMrQ-.mjs} +35 -32
- package/dist/exports-ZsgjzEnM.d.mts +4 -0
- package/dist/external-BV_cUncs.mjs +151 -0
- package/dist/folds-CRYY14kc.d.mts +534 -0
- package/dist/{reducers-mUPFuFJV.mjs → folds-Dyjput8M.mjs} +6 -73
- package/dist/ids-C6AYo1bu.mjs +31 -0
- package/dist/index.d.mts +4 -4
- package/dist/{index.default-Bkg77Pxn.mjs → index.default-C5Cpz3sa.mjs} +1 -1
- package/dist/index.mjs +5 -5
- package/dist/index.node.d.mts +4 -4
- package/dist/index.node.mjs +3 -3
- package/dist/inspect/app.js +31 -31
- package/dist/inspect/engine.js +5 -5
- package/dist/internal-CvsrprnU-CRY89I-b.mjs +203 -0
- package/dist/{local-Cr_dSjvs.mjs → local-DhkrAqPP.mjs} +1 -1
- package/dist/mentions-CB6ShY4R.mjs +72 -0
- package/dist/{targets-yM5JBUgr.mjs → middleware-CqaCwucP.mjs} +1 -33
- package/dist/platform-B4TnJtWu-CwcTyKg2.mjs +32 -0
- package/dist/{project-CFiArraX.mjs → project-DMDST9kl.mjs} +1 -1
- package/dist/{provider-BGjz-mDa.d.mts → provider-B0b9Qxw4.d.mts} +170 -40
- package/dist/provider-B3DpYSDq.mjs +220 -0
- package/dist/redis-tables-DN9PfIDi.mjs +3897 -0
- package/dist/reducer-CYPHsSmd-BQIfozGi.d.mts +170 -0
- package/dist/reducer-CjmvZSAb-D_OSISmj.mjs +74 -0
- package/dist/{contract-CuJmlrhw.mjs → reducers-BFmIXilu.mjs} +260 -287
- package/dist/retryable-lazy-DZWmHpii-Bye-W8q7.mjs +19 -0
- package/dist/scheduler-task-X1SToDjP-BVoWuInI.mjs +69 -0
- package/dist/{serve-BsOOc-6_.mjs → serve-BGYw0uNL.mjs} +5 -5
- package/dist/{server-jwPY7_wi.mjs → server-DPxBSogx.mjs} +4 -4
- package/dist/server-DtFrWxi3-Boa6gdWO.mjs +3602 -0
- package/dist/server-qgez0Xny-D5mLfqFQ.d.mts +385 -0
- package/dist/{serving-BNbzc96L.mjs → serving-CNkgUPW1.mjs} +1 -1
- package/dist/session-clear-wHd5GCpZ-BnKSxReK.mjs +51 -0
- package/dist/{sql-DbiEnqM0.mjs → sql-U9WTxrwJ.mjs} +2 -2
- package/dist/{sqlite-db-Cbj03vmp.mjs → sqlite-db-Ds4majtT.mjs} +1 -1
- package/dist/store-D0MhBvg6-4Z2L0Kqd.d.mts +530 -0
- package/dist/store-N8PXxDAS-BWBgzBHp.mjs +5 -0
- package/dist/store-codec-DwrWlgQx-CYxGW0aD.mjs +61 -0
- package/dist/store-memory-Bas9Uq5C.mjs +872 -0
- package/dist/store-memory-ZLiZbH3R.mjs +2 -0
- package/dist/store-polling-BZt2Rf3D-BiEM6_HB.mjs +148 -0
- package/dist/store-presence-polling-JuAy2Ck2-CYpuK75X.mjs +109 -0
- package/dist/store-sqlite-BmwL3pUR.mjs +2 -0
- package/dist/store-sqlite-CxxPCcCT.mjs +1053 -0
- package/dist/stream-buffer-DrTyiIuo-BKy-vI14.mjs +53 -0
- package/dist/sync-github.d.mts +24 -17
- package/dist/sync-github.mjs +89 -22
- package/dist/sync-kapso.d.mts +15 -13
- package/dist/sync-kapso.mjs +17 -14
- package/dist/sync-linear.d.mts +219 -0
- package/dist/sync-linear.mjs +311 -0
- package/dist/sync-slack.d.mts +12 -17
- package/dist/sync-slack.mjs +54 -42
- package/dist/sync.chat.d.mts +1 -1
- package/dist/sync.chat.mjs +1 -1
- package/dist/sync.d.mts +3 -3
- package/dist/sync.mjs +5 -4
- package/dist/targets-C-wdR5wP.mjs +33 -0
- package/dist/{tokens-DKLn5P9B.mjs → tokens-DhuzKzAl.mjs} +1 -1
- package/dist/{types-BHCjTZdB.d.mts → types-8yq2VnKI.d.mts} +2 -2
- package/dist/{users-Dls7_ANV.mjs → users-yryHKFMo.mjs} +2 -2
- package/dist/{watch-spec-W0c7ypHJ.mjs → watchers-hash-RLtnSF-t.mjs} +233 -28
- package/docs/01-board/01-mounting.md +1 -1
- package/docs/02-sync/01-overview.md +72 -48
- package/docs/02-sync/02-functions.md +398 -0
- package/docs/02-sync/03-slack.md +30 -25
- package/docs/02-sync/04-github.md +62 -26
- package/docs/02-sync/05-whatsapp.md +23 -19
- package/docs/02-sync/06-providers.md +60 -27
- package/docs/02-sync/07-linear.md +129 -0
- package/docs/03-guides/04-triage.md +1 -1
- package/docs/04-reference/01-cli.md +26 -6
- package/docs/04-reference/02-board.md +18 -5
- package/docs/04-reference/03-http.md +5 -2
- package/docs/04-reference/05-conformance.md +1 -1
- package/docs/index.md +5 -10
- package/package.json +31 -6
- package/dist/a2-D24cvwpN.d.mts +0 -3359
- package/dist/access-CJU2-vaa.mjs +0 -226
- package/dist/attempts-Bxp9KgPf.mjs +0 -919
- package/dist/exports-DLiE7fLY.d.mts +0 -4
- package/dist/provider-CuewcDNZ.mjs +0 -205
- package/dist/redis-tables-B35vv4CZ.mjs +0 -842
- package/dist/reducers-B_vUGQRp.d.mts +0 -1172
- package/dist/watchers-hash-CLjsa6od.mjs +0 -225
- package/docs/02-sync/02-callbacks.md +0 -265
|
@@ -1,842 +0,0 @@
|
|
|
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/redis-tables.ts
|
|
5
|
-
/**
|
|
6
|
-
* Applies an event's writes if it's the one to apply: with `next`, only the very
|
|
7
|
-
* next index; otherwise any index past the position. The check and the writes are
|
|
8
|
-
* one script, so a retried or superseded attempt changes nothing. Every key it
|
|
9
|
-
* touches comes in KEYS: the position first, then the writes' own.
|
|
10
|
-
*/
|
|
11
|
-
const APPLY = `
|
|
12
|
-
local pos = tonumber(redis.call('GET', KEYS[1]) or '-1')
|
|
13
|
-
local index = tonumber(ARGV[1])
|
|
14
|
-
if ARGV[2] == 'next' then
|
|
15
|
-
if pos ~= index - 1 then return 0 end
|
|
16
|
-
elseif pos >= index then
|
|
17
|
-
return 0
|
|
18
|
-
end
|
|
19
|
-
for _, op in ipairs(cjson.decode(ARGV[3])) do
|
|
20
|
-
redis.call(op[1], KEYS[op[2]], unpack(op, 3))
|
|
21
|
-
end
|
|
22
|
-
redis.call('SET', KEYS[1], ARGV[1])
|
|
23
|
-
return 1
|
|
24
|
-
`;
|
|
25
|
-
/**
|
|
26
|
-
* The scripts' shared part: which of a page of IDs match every group (one of
|
|
27
|
-
* its sets each) and the text. Upstash bills each command a script runs, so a
|
|
28
|
-
* page costs one ZMSCORE per set and one HMGET for the text, whatever its size.
|
|
29
|
-
*/
|
|
30
|
-
const MATCHES = `
|
|
31
|
-
local function matching(spec, from, ids)
|
|
32
|
-
local ok = {}
|
|
33
|
-
for i = 1, #ids do ok[i] = true end
|
|
34
|
-
for _, group in ipairs(spec.groups) do
|
|
35
|
-
-- The set the page came from holds every ID in it.
|
|
36
|
-
if not (#group == 1 and KEYS[group[1]] == from) then
|
|
37
|
-
local any = {}
|
|
38
|
-
for _, key in ipairs(group) do
|
|
39
|
-
local scores = redis.call('ZMSCORE', KEYS[key], unpack(ids))
|
|
40
|
-
for i = 1, #ids do if scores[i] then any[i] = true end end
|
|
41
|
-
end
|
|
42
|
-
for i = 1, #ids do if not any[i] then ok[i] = false end end
|
|
43
|
-
end
|
|
44
|
-
end
|
|
45
|
-
local metas = nil
|
|
46
|
-
if spec.text ~= cjson.null then
|
|
47
|
-
metas = redis.call('HMGET', KEYS[3], unpack(ids))
|
|
48
|
-
for i = 1, #ids do
|
|
49
|
-
if ok[i] and not string.find(cjson.decode(metas[i]).t, spec.text, 1, true) then ok[i] = false end
|
|
50
|
-
end
|
|
51
|
-
end
|
|
52
|
-
return ok, metas
|
|
53
|
-
end
|
|
54
|
-
local function driver(spec)
|
|
55
|
-
local best, size = KEYS[spec.fallback], nil
|
|
56
|
-
for _, key in ipairs(spec.drivers) do
|
|
57
|
-
local n = redis.call('ZCARD', KEYS[key])
|
|
58
|
-
if size == nil or n < size then best, size = KEYS[key], n end
|
|
59
|
-
end
|
|
60
|
-
return best
|
|
61
|
-
end
|
|
62
|
-
`;
|
|
63
|
-
/**
|
|
64
|
-
* Messages that match, walking the smallest index set newest first (oldest first
|
|
65
|
-
* with \`asc\`) and testing the others a page at a time, until \`limit\` or until
|
|
66
|
-
* it has read \`budget\` IDs: Redis runs nothing else while a script runs, so a
|
|
67
|
-
* long read goes in several. Returns the position, the matches, and where to go
|
|
68
|
-
* on from (false when the set ran out). KEYS: position, messages, metas, then
|
|
69
|
-
* the sets.
|
|
70
|
-
*/
|
|
71
|
-
const MESSAGES = `${MATCHES}
|
|
72
|
-
local spec = cjson.decode(ARGV[1])
|
|
73
|
-
local from = driver(spec)
|
|
74
|
-
local found, n, read = {}, 0, 0
|
|
75
|
-
local low, high = spec.min, spec.max
|
|
76
|
-
local more = true
|
|
77
|
-
while n < spec.limit do
|
|
78
|
-
if read >= spec.budget then break end
|
|
79
|
-
local count = math.min(200, spec.budget - read)
|
|
80
|
-
local ids
|
|
81
|
-
if spec.asc then ids = redis.call('ZRANGEBYSCORE', from, low, high, 'LIMIT', 0, count)
|
|
82
|
-
else ids = redis.call('ZREVRANGEBYSCORE', from, high, low, 'LIMIT', 0, count) end
|
|
83
|
-
if #ids == 0 then more = false break end
|
|
84
|
-
local ok = matching(spec, from, ids)
|
|
85
|
-
for i, id in ipairs(ids) do
|
|
86
|
-
read = read + 1
|
|
87
|
-
if spec.asc then low = '(' .. id else high = '(' .. id end
|
|
88
|
-
if ok[i] then
|
|
89
|
-
n = n + 1
|
|
90
|
-
found[n] = id
|
|
91
|
-
if n >= spec.limit then break end
|
|
92
|
-
end
|
|
93
|
-
end
|
|
94
|
-
end
|
|
95
|
-
local rows = {}
|
|
96
|
-
if n > 0 then rows = redis.call('HMGET', KEYS[2], unpack(found)) end
|
|
97
|
-
local next = false
|
|
98
|
-
if more and n < spec.limit then if spec.asc then next = low else next = high end end
|
|
99
|
-
return { redis.call('GET', KEYS[1]) or '0', rows, next }
|
|
100
|
-
`;
|
|
101
|
-
/**
|
|
102
|
-
* The latest threads in a channel, or across several, from their thread order
|
|
103
|
-
* sets: each set's first \`limit\`, merged. KEYS: position, then the sets.
|
|
104
|
-
*/
|
|
105
|
-
const LATEST = `
|
|
106
|
-
local spec = cjson.decode(ARGV[1])
|
|
107
|
-
local roots = {}
|
|
108
|
-
for i = 2, #KEYS do
|
|
109
|
-
local ids
|
|
110
|
-
if spec.asc then ids = redis.call('ZRANGEBYSCORE', KEYS[i], spec.min, '+inf', 'WITHSCORES', 'LIMIT', 0, spec.limit)
|
|
111
|
-
else ids = redis.call('ZREVRANGEBYSCORE', KEYS[i], '+inf', spec.min, 'WITHSCORES', 'LIMIT', 0, spec.limit) end
|
|
112
|
-
for j = 1, #ids, 2 do roots[#roots + 1] = ids[j] roots[#roots + 1] = ids[j + 1] end
|
|
113
|
-
end
|
|
114
|
-
return { redis.call('GET', KEYS[1]) or '0', roots }
|
|
115
|
-
`;
|
|
116
|
-
/**
|
|
117
|
-
* The threads with a message that matches: every match's root, once, and its
|
|
118
|
-
* latest reply, reading at most \`budget\` IDs. Returns the position, the roots
|
|
119
|
-
* and their latest replies, and where to go on from. KEYS: position, messages,
|
|
120
|
-
* metas, summaries, then the sets.
|
|
121
|
-
*/
|
|
122
|
-
const ROOTS = `${MATCHES}
|
|
123
|
-
local spec = cjson.decode(ARGV[1])
|
|
124
|
-
local from = driver(spec)
|
|
125
|
-
local roots, seen, read, high = {}, {}, 0, spec.max
|
|
126
|
-
while read < spec.budget do
|
|
127
|
-
local ids = redis.call('ZREVRANGEBYSCORE', from, high, '-inf', 'LIMIT', 0, math.min(200, spec.budget - read))
|
|
128
|
-
if #ids == 0 then return { redis.call('GET', KEYS[1]) or '0', roots, false } end
|
|
129
|
-
local ok, metas = matching(spec, from, ids)
|
|
130
|
-
local matched = {}
|
|
131
|
-
for i, id in ipairs(ids) do if ok[i] then matched[#matched + 1] = id end end
|
|
132
|
-
if #matched > 0 then
|
|
133
|
-
if metas then
|
|
134
|
-
local byId = {}
|
|
135
|
-
for i, id in ipairs(ids) do byId[id] = metas[i] end
|
|
136
|
-
metas = {}
|
|
137
|
-
for j, id in ipairs(matched) do metas[j] = byId[id] end
|
|
138
|
-
else
|
|
139
|
-
metas = redis.call('HMGET', KEYS[3], unpack(matched))
|
|
140
|
-
end
|
|
141
|
-
local fresh = {}
|
|
142
|
-
for j, id in ipairs(matched) do
|
|
143
|
-
local meta = cjson.decode(metas[j])
|
|
144
|
-
local root = id
|
|
145
|
-
if meta.re ~= cjson.null then root = tostring(meta.re) end
|
|
146
|
-
if not seen[root] then
|
|
147
|
-
seen[root] = true
|
|
148
|
-
fresh[#fresh + 1] = root
|
|
149
|
-
end
|
|
150
|
-
end
|
|
151
|
-
if #fresh > 0 then
|
|
152
|
-
local summaries = redis.call('HMGET', KEYS[4], unpack(fresh))
|
|
153
|
-
for j, root in ipairs(fresh) do
|
|
154
|
-
roots[#roots + 1] = root
|
|
155
|
-
roots[#roots + 1] = cjson.decode(summaries[j]).at
|
|
156
|
-
end
|
|
157
|
-
end
|
|
158
|
-
end
|
|
159
|
-
read = read + #ids
|
|
160
|
-
high = '(' .. ids[#ids]
|
|
161
|
-
end
|
|
162
|
-
return { redis.call('GET', KEYS[1]) or '0', roots, high }
|
|
163
|
-
`;
|
|
164
|
-
/** Each root, its reply count, and its latest reply (false when it has none). KEYS: messages, summaries. ARGV: the roots. */
|
|
165
|
-
const THREAD_ROWS = `
|
|
166
|
-
local summaries = redis.call('HMGET', KEYS[2], unpack(ARGV))
|
|
167
|
-
local lasts, slot = {}, {}
|
|
168
|
-
for i, root in ipairs(ARGV) do
|
|
169
|
-
summaries[i] = cjson.decode(summaries[i])
|
|
170
|
-
if tostring(summaries[i].at) ~= root then
|
|
171
|
-
lasts[#lasts + 1] = tostring(summaries[i].at)
|
|
172
|
-
slot[i] = #lasts
|
|
173
|
-
end
|
|
174
|
-
end
|
|
175
|
-
local rows = redis.call('HMGET', KEYS[1], unpack(ARGV))
|
|
176
|
-
local lastRows = {}
|
|
177
|
-
if #lasts > 0 then lastRows = redis.call('HMGET', KEYS[1], unpack(lasts)) end
|
|
178
|
-
local out = {}
|
|
179
|
-
for i = 1, #ARGV do
|
|
180
|
-
local last = false
|
|
181
|
-
if slot[i] then last = lastRows[slot[i]] end
|
|
182
|
-
out[i] = { rows[i], summaries[i].n, last }
|
|
183
|
-
end
|
|
184
|
-
return out
|
|
185
|
-
`;
|
|
186
|
-
/** Each message's latest version at or before ARGV[1]. KEYS: the version data, then each message's versions. */
|
|
187
|
-
const AT = `
|
|
188
|
-
local versions, slot = {}, {}
|
|
189
|
-
for i = 2, #KEYS do
|
|
190
|
-
local found = redis.call('ZREVRANGEBYSCORE', KEYS[i], ARGV[1], '-inf', 'LIMIT', 0, 1)
|
|
191
|
-
if #found > 0 then
|
|
192
|
-
versions[#versions + 1] = found[1]
|
|
193
|
-
slot[i - 1] = #versions
|
|
194
|
-
end
|
|
195
|
-
end
|
|
196
|
-
local data = {}
|
|
197
|
-
if #versions > 0 then data = redis.call('HMGET', KEYS[1], unpack(versions)) end
|
|
198
|
-
local out = {}
|
|
199
|
-
for i = 1, #KEYS - 1 do
|
|
200
|
-
if slot[i] then out[i] = data[slot[i]] else out[i] = false end
|
|
201
|
-
end
|
|
202
|
-
return out
|
|
203
|
-
`;
|
|
204
|
-
/** Each key's hash, whole. */
|
|
205
|
-
const HASHES = `
|
|
206
|
-
local out = {}
|
|
207
|
-
for i = 1, #KEYS do out[i] = redis.call('HGETALL', KEYS[i]) end
|
|
208
|
-
return out
|
|
209
|
-
`;
|
|
210
|
-
/** A person's roots posted at or after ARGV[1]. KEYS: messages, their roots by creation time. */
|
|
211
|
-
const RECENT = `
|
|
212
|
-
local ids = redis.call('ZRANGEBYSCORE', KEYS[2], ARGV[1], '+inf')
|
|
213
|
-
if #ids == 0 then return {} end
|
|
214
|
-
return redis.call('HMGET', KEYS[1], unpack(ids))
|
|
215
|
-
`;
|
|
216
|
-
/**
|
|
217
|
-
* What an event reads before it applies, in one script so it reads the primary,
|
|
218
|
-
* where the position check runs: a replica can lag it. Nothing, when the
|
|
219
|
-
* position says it's not the one to apply (as APPLY checks). KEYS: position,
|
|
220
|
-
* messages, metas, summaries, sync threads, the copy's hash. ARGV: the index,
|
|
221
|
-
* the mode, the message, its root ('' to take the message's own), the sync
|
|
222
|
-
* thread, the copy ('' for none of each). Returns the message, its root's ID,
|
|
223
|
-
* the root, its summary, the sync thread, and the copy.
|
|
224
|
-
*/
|
|
225
|
-
const BEFORE = `
|
|
226
|
-
local pos = tonumber(redis.call('GET', KEYS[1]) or '-1')
|
|
227
|
-
local index = tonumber(ARGV[1])
|
|
228
|
-
if ARGV[2] == 'next' then
|
|
229
|
-
if pos ~= index - 1 then return false end
|
|
230
|
-
elseif pos >= index then
|
|
231
|
-
return false
|
|
232
|
-
end
|
|
233
|
-
local message, root = false, ARGV[4]
|
|
234
|
-
if ARGV[3] ~= '' then
|
|
235
|
-
message = redis.call('HGET', KEYS[2], ARGV[3])
|
|
236
|
-
if message and root == '' then
|
|
237
|
-
local re = cjson.decode(redis.call('HGET', KEYS[3], ARGV[3])).re
|
|
238
|
-
if re ~= cjson.null then root = tostring(re) end
|
|
239
|
-
end
|
|
240
|
-
end
|
|
241
|
-
local rootData, summary = false, false
|
|
242
|
-
if root ~= '' then
|
|
243
|
-
rootData = redis.call('HGET', KEYS[2], root)
|
|
244
|
-
summary = redis.call('HGET', KEYS[4], root)
|
|
245
|
-
end
|
|
246
|
-
local thread, copy = false, false
|
|
247
|
-
if ARGV[5] ~= '' then thread = redis.call('HGET', KEYS[5], ARGV[5]) end
|
|
248
|
-
if ARGV[6] ~= '' then copy = redis.call('HGET', KEYS[6], ARGV[6]) end
|
|
249
|
-
return { message, root, rootData, summary, thread, copy }
|
|
250
|
-
`;
|
|
251
|
-
/** The newest reply in a thread that isn't deleted, other than ARGV (false when none). KEYS: the thread, metas. */
|
|
252
|
-
const NEWEST = `
|
|
253
|
-
local skip = {}
|
|
254
|
-
for _, id in ipairs(ARGV) do skip[id] = true end
|
|
255
|
-
local high = '+inf'
|
|
256
|
-
while true do
|
|
257
|
-
local ids = redis.call('ZREVRANGEBYSCORE', KEYS[1], high, '-inf', 'LIMIT', 0, 100)
|
|
258
|
-
if #ids == 0 then return false end
|
|
259
|
-
local metas = redis.call('HMGET', KEYS[2], unpack(ids))
|
|
260
|
-
for i, id in ipairs(ids) do
|
|
261
|
-
if not skip[id] and not cjson.decode(metas[i]).d then return id end
|
|
262
|
-
end
|
|
263
|
-
high = '(' .. ids[#ids]
|
|
264
|
-
end
|
|
265
|
-
`;
|
|
266
|
-
/** Every message in a set of IDs (a thread, or those carrying an attachment), a thousand to an HMGET. KEYS: the set, messages. */
|
|
267
|
-
const THREAD = `
|
|
268
|
-
local ids = redis.call('ZRANGE', KEYS[1], 0, -1)
|
|
269
|
-
local rows = {}
|
|
270
|
-
for first = 1, #ids, 1000 do
|
|
271
|
-
local part = redis.call('HMGET', KEYS[2], unpack(ids, first, math.min(first + 999, #ids)))
|
|
272
|
-
for i = 1, #part do rows[#rows + 1] = part[i] end
|
|
273
|
-
end
|
|
274
|
-
return rows
|
|
275
|
-
`;
|
|
276
|
-
/** The position and every channel, read together. KEYS: position, channels. */
|
|
277
|
-
const CHANNELS = `
|
|
278
|
-
return { redis.call('GET', KEYS[1]) or '0', redis.call('SMEMBERS', KEYS[2]) }
|
|
279
|
-
`;
|
|
280
|
-
/**
|
|
281
|
-
* Upstash runs a script under a lock on the whole database unless the script
|
|
282
|
-
* sets `allow-key-locking`, and then locks only the hash tags of its KEYS and
|
|
283
|
-
* refuses any other key. Every key of the tables shares one tag, so a script
|
|
284
|
-
* locks only its board's tables; a `no-writes` one takes the shared lock. Redis
|
|
285
|
-
* and Valkey reject the flag, and have no such lock: one thread runs every
|
|
286
|
-
* command. A2's Redis store does the same for its sessions.
|
|
287
|
-
*/
|
|
288
|
-
const KEY_LOCKING = "#!lua flags=allow-key-locking";
|
|
289
|
-
const READ_ONLY_KEY_LOCKING = "#!lua flags=no-writes,allow-key-locking";
|
|
290
|
-
/** A server that doesn't know the flag (Redis 7, Valkey), or shebangs at all (Redis 6). */
|
|
291
|
-
const SHEBANG_REJECTED = /shebang|unexpected symbol near '#'/i;
|
|
292
|
-
const SEP = "";
|
|
293
|
-
/** How many IDs one query script reads before it hands back where it got to. */
|
|
294
|
-
const SCAN_BUDGET = 4e3;
|
|
295
|
-
/** JSON a script decodes: Redis's cjson refuses an escaped lone surrogate, so each goes out as U+FFFD. */
|
|
296
|
-
const luaJson = (value) => JSON.stringify(value, (_key, item) => typeof item === "string" ? item.toWellFormed() : item);
|
|
297
|
-
/** The label sets a message is in: each key, and each key with its value. */
|
|
298
|
-
const labelTokens = (labels) => Object.entries(labels).flatMap(([key, value]) => [`l${SEP}${key}`, `lv${SEP}${key}${SEP}${value}`]);
|
|
299
|
-
/** A message is in its own label sets and, for a reply, its root's: "on the message or its root" is one lookup. */
|
|
300
|
-
const inLabelSets = (own, root) => /* @__PURE__ */ new Set([...labelTokens(own), ...root ? labelTokens(root) : []]);
|
|
301
|
-
/** The tables' keys, `<prefix>:{<prefix>:board}:<name>`: one hash tag, its board's own. */
|
|
302
|
-
const tablesKeyBase = (prefix) => `${prefix}:{${prefix}:board}`;
|
|
303
|
-
/**
|
|
304
|
-
* The tables on Redis: messages, versions, and sync rows in hashes and sorted
|
|
305
|
-
* sets under `<prefix>:{<prefix>:board}`, and a sorted set per filter value,
|
|
306
|
-
* scored by message ID. Each event is two scripts, one that reads what it
|
|
307
|
-
* changes and one that checks the position and writes; each query is one
|
|
308
|
-
* script, which walks the smallest set that applies.
|
|
309
|
-
*
|
|
310
|
-
* Every key shares the hash tag `{<prefix>:board}`: the board's alone, since a
|
|
311
|
-
* prefix can't hold a brace, and never an A2 session's, which is a JSON string.
|
|
312
|
-
* Each script declares every key it touches. Keys of an earlier layout,
|
|
313
|
-
* `<prefix>:board:<name>`, have other names, so they're never read: the tables
|
|
314
|
-
* fill from the log.
|
|
315
|
-
*/
|
|
316
|
-
function redisTables(send, prefix, options = {}) {
|
|
317
|
-
if (prefix.includes("{") || prefix.includes("}")) throw new TypeError("a Redis keyPrefix can't contain braces: they'd set every key's hash tag");
|
|
318
|
-
const call = (command, ...args) => send(command, ...args.map((arg) => typeof arg === "string" ? arg.toWellFormed() : arg));
|
|
319
|
-
const budget = options.scanBudget ?? SCAN_BUDGET;
|
|
320
|
-
const p = tablesKeyBase(prefix);
|
|
321
|
-
const k = {
|
|
322
|
-
pos: `${p}:pos`,
|
|
323
|
-
messages: `${p}:m`,
|
|
324
|
-
metas: `${p}:x`,
|
|
325
|
-
summaries: `${p}:s`,
|
|
326
|
-
events: `${p}:e`,
|
|
327
|
-
versionData: `${p}:vd`,
|
|
328
|
-
all: `${p}:i:all`,
|
|
329
|
-
channels: `${p}:channels`,
|
|
330
|
-
threads: `${p}:t`,
|
|
331
|
-
syncThreads: `${p}:st`,
|
|
332
|
-
externalThreads: `${p}:et`,
|
|
333
|
-
externalMessages: `${p}:em`,
|
|
334
|
-
versions: (id) => `${p}:v:${id}`,
|
|
335
|
-
channel: (channel) => `${p}:i:c:${channel}`,
|
|
336
|
-
channelThreads: (channel) => `${p}:t:c:${channel}`,
|
|
337
|
-
/** A root's thread, it and its replies; a reply's holds just it. */
|
|
338
|
-
thread: (id) => `${p}:i:r:${id}`,
|
|
339
|
-
byActor: (actor) => `${p}:i:ba:${actor}`,
|
|
340
|
-
byPrincipal: (principal) => `${p}:i:bp:${principal}`,
|
|
341
|
-
mention: (actor) => `${p}:i:mn:${actor}`,
|
|
342
|
-
attachment: (id) => `${p}:i:at:${id}`,
|
|
343
|
-
from: (from) => `${p}:i:f:${from}`,
|
|
344
|
-
/** A person's roots, by when they were posted: for the echo check. */
|
|
345
|
-
rootsBy: (principal) => `${p}:rr:${principal}`,
|
|
346
|
-
label: (token) => `${p}:i:${token}`,
|
|
347
|
-
copies: (id) => `${p}:cp:${id}`
|
|
348
|
-
};
|
|
349
|
-
const parse = (value) => value === null || value === void 0 || value === false ? null : JSON.parse(String(value));
|
|
350
|
-
/**
|
|
351
|
-
* Whether the server takes `allow-key-locking`, asked once, before the first script. Only its
|
|
352
|
-
* rejection of the flag means no: a transport error decides nothing, and the next script asks again.
|
|
353
|
-
*/
|
|
354
|
-
let keyLocking = null;
|
|
355
|
-
const locking = () => keyLocking ??= call("EVAL", `${KEY_LOCKING}\nreturn 1`, 1, k.pos).then(() => true, (error) => {
|
|
356
|
-
if (SHEBANG_REJECTED.test(String(error?.message ?? error))) return false;
|
|
357
|
-
keyLocking = null;
|
|
358
|
-
throw error;
|
|
359
|
-
});
|
|
360
|
-
const run = async (flags, script, keys, args) => call("EVAL", await locking() ? `${flags}\n${script}` : script, keys.length, ...keys, ...args);
|
|
361
|
-
/** A query: on Upstash it takes the shared lock, beside other reads. */
|
|
362
|
-
const evaluate = (script, keys, args) => run(READ_ONLY_KEY_LOCKING, script, keys, args);
|
|
363
|
-
/**
|
|
364
|
-
* An event's apply, and the reads its writes are worked out from: they run as writing scripts,
|
|
365
|
-
* so they read where the write that checks the position runs, whatever a provider does with a
|
|
366
|
-
* read-only one.
|
|
367
|
-
*/
|
|
368
|
-
const evaluateWrite = (script, keys, args) => run(KEY_LOCKING, script, keys, args);
|
|
369
|
-
async function messagesOf(ids) {
|
|
370
|
-
const out = /* @__PURE__ */ new Map();
|
|
371
|
-
if (ids.length === 0) return out;
|
|
372
|
-
const rows = await call("HMGET", k.messages, ...ids);
|
|
373
|
-
ids.forEach((id, i) => {
|
|
374
|
-
const message = parse(rows[i]);
|
|
375
|
-
if (message) out.set(id, message);
|
|
376
|
-
});
|
|
377
|
-
return out;
|
|
378
|
-
}
|
|
379
|
-
/**
|
|
380
|
-
* What the event reads before it applies: its message, its root's summary and labels, and the
|
|
381
|
-
* sync rows it changes. Null when the position says it's not the one to apply.
|
|
382
|
-
*/
|
|
383
|
-
async function before(event, id, mode) {
|
|
384
|
-
const posted = event.type === "message.posted";
|
|
385
|
-
const reads = syncReads(event);
|
|
386
|
-
const keys = [
|
|
387
|
-
k.pos,
|
|
388
|
-
k.messages,
|
|
389
|
-
k.metas,
|
|
390
|
-
k.summaries,
|
|
391
|
-
k.syncThreads,
|
|
392
|
-
reads.copy === null ? k.pos : k.copies(reads.copy[0])
|
|
393
|
-
];
|
|
394
|
-
const args = [
|
|
395
|
-
event.index,
|
|
396
|
-
mode,
|
|
397
|
-
id === null || posted ? "" : String(id),
|
|
398
|
-
posted && event.payload.re !== null ? String(event.payload.re) : "",
|
|
399
|
-
reads.thread ?? "",
|
|
400
|
-
reads.copy?.[1] ?? ""
|
|
401
|
-
];
|
|
402
|
-
const found = await evaluateWrite(BEFORE, keys, args);
|
|
403
|
-
if (!found) return null;
|
|
404
|
-
const [data, rootId, rootData, rootSummary, thread, copy] = found;
|
|
405
|
-
const sync = {
|
|
406
|
-
thread: parse(thread) ?? void 0,
|
|
407
|
-
copy: parse(copy) ?? void 0
|
|
408
|
-
};
|
|
409
|
-
if (id === null) return {
|
|
410
|
-
read: null,
|
|
411
|
-
sync
|
|
412
|
-
};
|
|
413
|
-
const message = parse(data) ?? void 0;
|
|
414
|
-
const summary = parse(rootSummary);
|
|
415
|
-
const root = summary ? {
|
|
416
|
-
replies: summary.n,
|
|
417
|
-
at: summary.at
|
|
418
|
-
} : void 0;
|
|
419
|
-
const read = {
|
|
420
|
-
message,
|
|
421
|
-
root,
|
|
422
|
-
rootLabels: parse(rootData)?.labels ?? null
|
|
423
|
-
};
|
|
424
|
-
if (event.type === "message.deleted" && message && !message.deleted && root?.at === id) {
|
|
425
|
-
const newest = await evaluateWrite(NEWEST, [k.thread(Number(rootId)), k.metas], [String(rootId), String(id)]);
|
|
426
|
-
read.previousLast = newest === null || newest === void 0 || newest === false ? null : Number(newest);
|
|
427
|
-
}
|
|
428
|
-
return {
|
|
429
|
-
read,
|
|
430
|
-
sync
|
|
431
|
-
};
|
|
432
|
-
}
|
|
433
|
-
/** A root's replies, when its labels changed: they're in its label sets too. */
|
|
434
|
-
async function repliesOf(read, change) {
|
|
435
|
-
const after = change.message?.after;
|
|
436
|
-
if (!after || after.re !== null || change.message.created || !change.message.terms) return [];
|
|
437
|
-
if (JSON.stringify(after.labels) === JSON.stringify(read.message.labels)) return [];
|
|
438
|
-
return (await evaluateWrite(THREAD, [k.thread(after.id), k.messages], [])).flatMap((row) => {
|
|
439
|
-
const message = parse(row);
|
|
440
|
-
return message && message.id !== after.id ? [message] : [];
|
|
441
|
-
});
|
|
442
|
-
}
|
|
443
|
-
/** The event's writes, as commands. */
|
|
444
|
-
function writes(event, read, replies, change, sync) {
|
|
445
|
-
const out = [];
|
|
446
|
-
const m = change.message?.after;
|
|
447
|
-
if (m && read) {
|
|
448
|
-
const id = String(m.id);
|
|
449
|
-
const created = change.message.created;
|
|
450
|
-
const meta = {
|
|
451
|
-
re: m.re,
|
|
452
|
-
c: m.channel,
|
|
453
|
-
t: m.text.toLowerCase(),
|
|
454
|
-
ca: Date.parse(m.createdAt),
|
|
455
|
-
d: m.deleted
|
|
456
|
-
};
|
|
457
|
-
out.push([
|
|
458
|
-
"HSET",
|
|
459
|
-
k.messages,
|
|
460
|
-
id,
|
|
461
|
-
JSON.stringify(m)
|
|
462
|
-
], [
|
|
463
|
-
"HSET",
|
|
464
|
-
k.metas,
|
|
465
|
-
id,
|
|
466
|
-
luaJson(meta)
|
|
467
|
-
]);
|
|
468
|
-
out.push([
|
|
469
|
-
"ZADD",
|
|
470
|
-
k.versions(id),
|
|
471
|
-
String(event.index),
|
|
472
|
-
String(event.index)
|
|
473
|
-
], [
|
|
474
|
-
"HSET",
|
|
475
|
-
k.versionData,
|
|
476
|
-
String(event.index),
|
|
477
|
-
JSON.stringify(m)
|
|
478
|
-
]);
|
|
479
|
-
if (created) {
|
|
480
|
-
out.push([
|
|
481
|
-
"HSET",
|
|
482
|
-
k.events,
|
|
483
|
-
event.id,
|
|
484
|
-
id
|
|
485
|
-
], [
|
|
486
|
-
"SADD",
|
|
487
|
-
k.channels,
|
|
488
|
-
m.channel
|
|
489
|
-
]);
|
|
490
|
-
for (const key of [
|
|
491
|
-
k.all,
|
|
492
|
-
k.channel(m.channel),
|
|
493
|
-
k.thread(id),
|
|
494
|
-
k.byActor(m.by.actor),
|
|
495
|
-
k.byPrincipal(m.by.principal),
|
|
496
|
-
k.from(m.from ?? "board")
|
|
497
|
-
]) out.push([
|
|
498
|
-
"ZADD",
|
|
499
|
-
key,
|
|
500
|
-
id,
|
|
501
|
-
id
|
|
502
|
-
]);
|
|
503
|
-
if (m.re !== null) out.push([
|
|
504
|
-
"ZADD",
|
|
505
|
-
k.thread(m.re),
|
|
506
|
-
id,
|
|
507
|
-
id
|
|
508
|
-
]);
|
|
509
|
-
else out.push([
|
|
510
|
-
"ZADD",
|
|
511
|
-
k.rootsBy(m.by.principal),
|
|
512
|
-
String(meta.ca),
|
|
513
|
-
id
|
|
514
|
-
]);
|
|
515
|
-
}
|
|
516
|
-
if (change.message.terms) {
|
|
517
|
-
const old = read.message;
|
|
518
|
-
const rootLabels = m.re === null ? null : read.rootLabels;
|
|
519
|
-
const was = created ? /* @__PURE__ */ new Set() : inLabelSets(old.labels, rootLabels);
|
|
520
|
-
const is = inLabelSets(m.labels, rootLabels);
|
|
521
|
-
for (const token of was) if (!is.has(token)) out.push([
|
|
522
|
-
"ZREM",
|
|
523
|
-
k.label(token),
|
|
524
|
-
id
|
|
525
|
-
]);
|
|
526
|
-
for (const token of is) if (!was.has(token)) out.push([
|
|
527
|
-
"ZADD",
|
|
528
|
-
k.label(token),
|
|
529
|
-
id,
|
|
530
|
-
id
|
|
531
|
-
]);
|
|
532
|
-
const mentionedBefore = new Set(created ? [] : old.mentions);
|
|
533
|
-
const mentioned = new Set(m.mentions);
|
|
534
|
-
for (const actor of mentionedBefore) if (!mentioned.has(actor)) out.push([
|
|
535
|
-
"ZREM",
|
|
536
|
-
k.mention(actor),
|
|
537
|
-
id
|
|
538
|
-
]);
|
|
539
|
-
for (const actor of mentioned) if (!mentionedBefore.has(actor)) out.push([
|
|
540
|
-
"ZADD",
|
|
541
|
-
k.mention(actor),
|
|
542
|
-
id,
|
|
543
|
-
id
|
|
544
|
-
]);
|
|
545
|
-
const filesBefore = new Set(created ? [] : old.attachments.map((file) => file.id));
|
|
546
|
-
const files = new Set(m.attachments.map((file) => file.id));
|
|
547
|
-
for (const file of filesBefore) if (!files.has(file)) out.push([
|
|
548
|
-
"ZREM",
|
|
549
|
-
k.attachment(file),
|
|
550
|
-
id
|
|
551
|
-
]);
|
|
552
|
-
for (const file of files) if (!filesBefore.has(file)) out.push([
|
|
553
|
-
"ZADD",
|
|
554
|
-
k.attachment(file),
|
|
555
|
-
id,
|
|
556
|
-
id
|
|
557
|
-
]);
|
|
558
|
-
for (const reply of replies) {
|
|
559
|
-
const replyWas = inLabelSets(reply.labels, old.labels);
|
|
560
|
-
const replyIs = inLabelSets(reply.labels, m.labels);
|
|
561
|
-
for (const token of replyWas) if (!replyIs.has(token)) out.push([
|
|
562
|
-
"ZREM",
|
|
563
|
-
k.label(token),
|
|
564
|
-
String(reply.id)
|
|
565
|
-
]);
|
|
566
|
-
for (const token of replyIs) if (!replyWas.has(token)) out.push([
|
|
567
|
-
"ZADD",
|
|
568
|
-
k.label(token),
|
|
569
|
-
String(reply.id),
|
|
570
|
-
String(reply.id)
|
|
571
|
-
]);
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
if (change.summary) {
|
|
576
|
-
const { root, replies, at } = change.summary;
|
|
577
|
-
const channel = m?.channel ?? read?.message?.channel;
|
|
578
|
-
out.push([
|
|
579
|
-
"HSET",
|
|
580
|
-
k.summaries,
|
|
581
|
-
String(root),
|
|
582
|
-
JSON.stringify({
|
|
583
|
-
n: replies,
|
|
584
|
-
at
|
|
585
|
-
})
|
|
586
|
-
], [
|
|
587
|
-
"ZADD",
|
|
588
|
-
k.threads,
|
|
589
|
-
String(at),
|
|
590
|
-
String(root)
|
|
591
|
-
]);
|
|
592
|
-
if (channel !== void 0) out.push([
|
|
593
|
-
"ZADD",
|
|
594
|
-
k.channelThreads(channel),
|
|
595
|
-
String(at),
|
|
596
|
-
String(root)
|
|
597
|
-
]);
|
|
598
|
-
}
|
|
599
|
-
if (sync.thread) out.push([
|
|
600
|
-
"HSET",
|
|
601
|
-
k.syncThreads,
|
|
602
|
-
sync.thread.key,
|
|
603
|
-
JSON.stringify(sync.thread.data)
|
|
604
|
-
]);
|
|
605
|
-
for (const [external, thread] of sync.externalThreads) out.push([
|
|
606
|
-
"HSET",
|
|
607
|
-
k.externalThreads,
|
|
608
|
-
external,
|
|
609
|
-
thread
|
|
610
|
-
]);
|
|
611
|
-
for (const [message, target, copy] of sync.copies) out.push([
|
|
612
|
-
"HSET",
|
|
613
|
-
k.copies(message),
|
|
614
|
-
target,
|
|
615
|
-
JSON.stringify(copy)
|
|
616
|
-
]);
|
|
617
|
-
for (const [key, message] of sync.externalMessages) out.push([
|
|
618
|
-
"HSET",
|
|
619
|
-
k.externalMessages,
|
|
620
|
-
key,
|
|
621
|
-
String(message)
|
|
622
|
-
]);
|
|
623
|
-
return out;
|
|
624
|
-
}
|
|
625
|
-
async function commit(event, mode) {
|
|
626
|
-
const found = await before(event, concernedMessageId(event), mode);
|
|
627
|
-
if (!found) return false;
|
|
628
|
-
const { read, sync: syncRead } = found;
|
|
629
|
-
const change = read ? project(read, event) : {};
|
|
630
|
-
const ops = writes(event, read, read ? await repliesOf(read, change) : [], change, projectSync(syncRead, event));
|
|
631
|
-
const keys = [k.pos];
|
|
632
|
-
const slots = /* @__PURE__ */ new Map();
|
|
633
|
-
const encoded = ops.map(([command, key, ...args]) => {
|
|
634
|
-
if (!slots.has(key)) slots.set(key, keys.push(key));
|
|
635
|
-
return [
|
|
636
|
-
command,
|
|
637
|
-
slots.get(key),
|
|
638
|
-
...args
|
|
639
|
-
];
|
|
640
|
-
});
|
|
641
|
-
return Number(await evaluateWrite(APPLY, keys, [
|
|
642
|
-
event.index,
|
|
643
|
-
mode,
|
|
644
|
-
luaJson(encoded)
|
|
645
|
-
])) === 1;
|
|
646
|
-
}
|
|
647
|
-
/**
|
|
648
|
-
* The channels under `path`, and the position that list reflects. A read's
|
|
649
|
-
* position goes no further: a post in a channel newer than the list is past
|
|
650
|
-
* it, so the feed from that position carries it.
|
|
651
|
-
*/
|
|
652
|
-
async function channelsUnder(path) {
|
|
653
|
-
const [at, all] = await evaluate(CHANNELS, [k.pos, k.channels], []);
|
|
654
|
-
return {
|
|
655
|
-
under: all.filter((c) => c.startsWith(`${path}/`)),
|
|
656
|
-
asOf: Number(at)
|
|
657
|
-
};
|
|
658
|
-
}
|
|
659
|
-
/** A filter as groups of sets, each group needing one: the query scripts' input, and the position it holds for. */
|
|
660
|
-
async function compile(filter, keys) {
|
|
661
|
-
const slot = (key) => keys.push(key);
|
|
662
|
-
const groups = [];
|
|
663
|
-
const group = (sets) => groups.push(sets.map(slot));
|
|
664
|
-
let asOf = Number.POSITIVE_INFINITY;
|
|
665
|
-
if (filter.channel) {
|
|
666
|
-
const { path, subtree } = filter.channel;
|
|
667
|
-
let under = [];
|
|
668
|
-
if (subtree) ({under, asOf} = await channelsUnder(path));
|
|
669
|
-
group([path, ...under].map(k.channel));
|
|
670
|
-
}
|
|
671
|
-
if (filter.channels) group(filter.channels.map(k.channel));
|
|
672
|
-
if (filter.re !== void 0) group([k.thread(filter.re)]);
|
|
673
|
-
if (filter.by) group([...filter.by.actors.map(k.byActor), ...filter.by.principals.map(k.byPrincipal)]);
|
|
674
|
-
if (filter.mentions) group(filter.mentions.map(k.mention));
|
|
675
|
-
if (filter.from !== void 0) group([k.from(filter.from)]);
|
|
676
|
-
for (const label of filter.labels ?? []) group(label.values === null ? [k.label(`l${SEP}${label.key}`)] : label.values.map((value) => k.label(`lv${SEP}${label.key}${SEP}${value}`)));
|
|
677
|
-
return {
|
|
678
|
-
spec: {
|
|
679
|
-
groups,
|
|
680
|
-
drivers: groups.filter((sets) => sets.length === 1).map((sets) => sets[0]),
|
|
681
|
-
fallback: slot(k.all),
|
|
682
|
-
text: filter.text ?? null
|
|
683
|
-
},
|
|
684
|
-
asOf
|
|
685
|
-
};
|
|
686
|
-
}
|
|
687
|
-
return {
|
|
688
|
-
async position() {
|
|
689
|
-
return Number(await call("GET", k.pos) ?? 0);
|
|
690
|
-
},
|
|
691
|
-
async apply(event) {
|
|
692
|
-
await commit(event, "any");
|
|
693
|
-
},
|
|
694
|
-
applyNext: (event) => commit(event, "next"),
|
|
695
|
-
async message(id) {
|
|
696
|
-
return (await messagesOf([id])).get(id) ?? null;
|
|
697
|
-
},
|
|
698
|
-
async byEventId(eventId) {
|
|
699
|
-
const id = await call("HGET", k.events, eventId);
|
|
700
|
-
return id === null || id === void 0 ? null : (await messagesOf([Number(id)])).get(Number(id)) ?? null;
|
|
701
|
-
},
|
|
702
|
-
async messagesAt(ids, index) {
|
|
703
|
-
const out = /* @__PURE__ */ new Map();
|
|
704
|
-
const wanted = [...new Set(ids)];
|
|
705
|
-
if (wanted.length === 0) return out;
|
|
706
|
-
const rows = await evaluate(AT, [k.versionData, ...wanted.map(k.versions)], [index]);
|
|
707
|
-
wanted.forEach((id, i) => {
|
|
708
|
-
const message = parse(rows[i]);
|
|
709
|
-
if (message) out.set(id, message);
|
|
710
|
-
});
|
|
711
|
-
return out;
|
|
712
|
-
},
|
|
713
|
-
async thread(root) {
|
|
714
|
-
const ids = (await call("ZRANGE", k.thread(root), 0, -1)).map(Number);
|
|
715
|
-
const found = await messagesOf(ids);
|
|
716
|
-
return ids.flatMap((id) => found.has(id) ? [found.get(id)] : []);
|
|
717
|
-
},
|
|
718
|
-
async messages(filter, page) {
|
|
719
|
-
const keys = [
|
|
720
|
-
k.pos,
|
|
721
|
-
k.messages,
|
|
722
|
-
k.metas
|
|
723
|
-
];
|
|
724
|
-
const asc = page.after !== void 0;
|
|
725
|
-
const { spec: compiled, asOf } = await compile(filter, keys);
|
|
726
|
-
const spec = {
|
|
727
|
-
...compiled,
|
|
728
|
-
limit: page.limit,
|
|
729
|
-
budget,
|
|
730
|
-
asc,
|
|
731
|
-
min: asc ? `(${page.after}` : "-inf",
|
|
732
|
-
max: page.before !== void 0 ? `(${page.before}` : "+inf"
|
|
733
|
-
};
|
|
734
|
-
let position = null;
|
|
735
|
-
const messages = [];
|
|
736
|
-
for (;;) {
|
|
737
|
-
const [at, rows, next] = await evaluate(MESSAGES, keys, [luaJson(spec)]);
|
|
738
|
-
position ??= Number(at);
|
|
739
|
-
for (const row of rows) messages.push(parse(row));
|
|
740
|
-
spec.limit = page.limit - messages.length;
|
|
741
|
-
if (next === null || spec.limit === 0) break;
|
|
742
|
-
if (asc) spec.min = next;
|
|
743
|
-
else spec.max = next;
|
|
744
|
-
}
|
|
745
|
-
return {
|
|
746
|
-
messages: asc ? messages : messages.reverse(),
|
|
747
|
-
position: Math.min(position, asOf)
|
|
748
|
-
};
|
|
749
|
-
},
|
|
750
|
-
async threads(filter, page) {
|
|
751
|
-
const asc = page.after !== void 0;
|
|
752
|
-
const { channel, channels, ...rest } = filter;
|
|
753
|
-
let position = null;
|
|
754
|
-
const roots = /* @__PURE__ */ new Map();
|
|
755
|
-
let asOf = Number.POSITIVE_INFINITY;
|
|
756
|
-
if (Object.keys(rest).length === 0 && !(channel && channels)) {
|
|
757
|
-
let under = [];
|
|
758
|
-
if (channel?.subtree) ({under, asOf} = await channelsUnder(channel.path));
|
|
759
|
-
const sets = channels ? channels.map(k.channelThreads) : channel ? [channel.path, ...under].map(k.channelThreads) : [k.threads];
|
|
760
|
-
const spec = {
|
|
761
|
-
limit: page.limit,
|
|
762
|
-
asc,
|
|
763
|
-
min: asc ? `(${page.after}` : "-inf"
|
|
764
|
-
};
|
|
765
|
-
const [at, pairs] = await evaluate(LATEST, [k.pos, ...sets], [luaJson(spec)]);
|
|
766
|
-
position = Number(at);
|
|
767
|
-
for (let i = 0; i < pairs.length; i += 2) roots.set(pairs[i], Number(pairs[i + 1]));
|
|
768
|
-
} else {
|
|
769
|
-
const keys = [
|
|
770
|
-
k.pos,
|
|
771
|
-
k.messages,
|
|
772
|
-
k.metas,
|
|
773
|
-
k.summaries
|
|
774
|
-
];
|
|
775
|
-
const compiled = await compile(filter, keys);
|
|
776
|
-
asOf = compiled.asOf;
|
|
777
|
-
const spec = {
|
|
778
|
-
...compiled.spec,
|
|
779
|
-
budget,
|
|
780
|
-
max: "+inf"
|
|
781
|
-
};
|
|
782
|
-
for (;;) {
|
|
783
|
-
const [at, pairs, next] = await evaluate(ROOTS, keys, [luaJson(spec)]);
|
|
784
|
-
position ??= Number(at);
|
|
785
|
-
for (let i = 0; i < pairs.length; i += 2) roots.set(String(pairs[i]), Number(pairs[i + 1]));
|
|
786
|
-
if (next === null) break;
|
|
787
|
-
spec.max = next;
|
|
788
|
-
}
|
|
789
|
-
}
|
|
790
|
-
const order = [...roots].filter(([, at]) => page.after === void 0 || at > page.after).sort(([, a], [, b]) => asc ? a - b : b - a).slice(0, page.limit).map(([root]) => root);
|
|
791
|
-
const threads = (order.length === 0 ? [] : await evaluate(THREAD_ROWS, [k.messages, k.summaries], order)).map(([root, replies, last]) => ({
|
|
792
|
-
root: parse(root),
|
|
793
|
-
replies: Number(replies),
|
|
794
|
-
last: parse(last)
|
|
795
|
-
}));
|
|
796
|
-
return {
|
|
797
|
-
threads: asc ? threads : threads.reverse(),
|
|
798
|
-
position: Math.min(position, asOf)
|
|
799
|
-
};
|
|
800
|
-
},
|
|
801
|
-
async syncOf(thread) {
|
|
802
|
-
return parse(await call("HGET", k.syncThreads, thread));
|
|
803
|
-
},
|
|
804
|
-
async threadOf(externalThread) {
|
|
805
|
-
const thread = await call("HGET", k.externalThreads, externalThread);
|
|
806
|
-
return thread === null || thread === void 0 ? null : String(thread);
|
|
807
|
-
},
|
|
808
|
-
async copiesOf(ids) {
|
|
809
|
-
const out = /* @__PURE__ */ new Map();
|
|
810
|
-
const wanted = [...new Set(ids)];
|
|
811
|
-
if (wanted.length === 0) return out;
|
|
812
|
-
const rows = await evaluate(HASHES, wanted.map(k.copies), []);
|
|
813
|
-
wanted.forEach((id, i) => {
|
|
814
|
-
const flat = rows[i] ?? [];
|
|
815
|
-
if (flat.length === 0) return;
|
|
816
|
-
const copies = {};
|
|
817
|
-
for (let at = 0; at < flat.length; at += 2) setOwn(copies, flat[at], JSON.parse(flat[at + 1]));
|
|
818
|
-
out.set(id, copies);
|
|
819
|
-
});
|
|
820
|
-
return out;
|
|
821
|
-
},
|
|
822
|
-
async messageOf(externalThread, platformMessage) {
|
|
823
|
-
const id = await call("HGET", k.externalMessages, externalMessageKey(externalThread, platformMessage));
|
|
824
|
-
return id === null || id === void 0 ? null : Number(id);
|
|
825
|
-
},
|
|
826
|
-
async externalThreads() {
|
|
827
|
-
return await call("HKEYS", k.externalThreads);
|
|
828
|
-
},
|
|
829
|
-
async withAttachment(attachment) {
|
|
830
|
-
return (await evaluate(THREAD, [k.attachment(attachment), k.messages], [])).flatMap((row) => {
|
|
831
|
-
const message = parse(row);
|
|
832
|
-
return message ? [message] : [];
|
|
833
|
-
});
|
|
834
|
-
},
|
|
835
|
-
async recentRoots(principal, since) {
|
|
836
|
-
return (await evaluate(RECENT, [k.messages, k.rootsBy(principal)], [since])).map((row) => parse(row)).sort((a, b) => a.id - b.id);
|
|
837
|
-
},
|
|
838
|
-
async close() {}
|
|
839
|
-
};
|
|
840
|
-
}
|
|
841
|
-
//#endregion
|
|
842
|
-
export { redisTables as t };
|