@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.
Files changed (130) hide show
  1. package/README.md +27 -27
  2. package/dist/{a2-C41hD8SF.mjs → a2-ChRLRupa.mjs} +969 -25
  3. package/dist/access.d.mts +1 -1
  4. package/dist/access.mjs +3 -3
  5. package/dist/{actors-DjMeZpnU.mjs → actors-BOamkEuz.mjs} +3 -73
  6. package/dist/attempts-CLG840Oj.mjs +20 -0
  7. package/dist/{auth-COzc09sn.mjs → auth-Drg9FiC2.mjs} +225 -2
  8. package/dist/backend-B-SJ7p6p.d.mts +680 -0
  9. package/dist/backend-a2/contract.d.mts +873 -2
  10. package/dist/backend-a2/contract.mjs +2 -2
  11. package/dist/backend-a2/scheduler-local.d.mts +1 -1
  12. package/dist/backend-a2/scheduler-local.mjs +1 -1
  13. package/dist/backend-a2/scheduler-qstash.d.mts +78 -1
  14. package/dist/backend-a2/scheduler-qstash.mjs +521 -1
  15. package/dist/backend-a2/scheduler-vercel.d.mts +37 -1
  16. package/dist/backend-a2/scheduler-vercel.mjs +203 -1
  17. package/dist/backend-a2/store-memory.d.mts +2 -2
  18. package/dist/backend-a2/store-memory.mjs +3 -3
  19. package/dist/backend-a2/store-postgres.d.mts +1 -1
  20. package/dist/backend-a2/store-postgres.mjs +2850 -2
  21. package/dist/backend-a2/store-redis-http.d.mts +1 -1
  22. package/dist/backend-a2/store-redis-http.mjs +490 -2
  23. package/dist/backend-a2/store-redis.d.mts +10 -2
  24. package/dist/backend-a2/store-redis.mjs +51 -2
  25. package/dist/backend-a2/store-sqlite.d.mts +1 -1
  26. package/dist/backend-a2/store-sqlite.mjs +3 -3
  27. package/dist/backend-a2.d.mts +2706 -3
  28. package/dist/backend-a2.mjs +1 -1
  29. package/dist/{backend-http-Bid7RIpC.d.mts → backend-http-7cNb8nJI.d.mts} +10 -3
  30. package/dist/{backend-http-0roEmjl0.mjs → backend-http-CMlL3yq3.mjs} +2 -2
  31. package/dist/{backend-http-BVRno-ca.mjs → backend-http-Ddzd5kTS.mjs} +32 -3
  32. package/dist/backend-http.d.mts +1 -1
  33. package/dist/backend-http.mjs +1 -1
  34. package/dist/backend-http.node.d.mts +1 -1
  35. package/dist/backend-http.node.mjs +1 -1
  36. package/dist/{chat-provider-XCnQlxq6.d.mts → chat-provider-DNJQyajS.d.mts} +16 -4
  37. package/dist/{chat-provider-jFSeyNmB.mjs → chat-provider-YHy-WbrB.mjs} +15 -9
  38. package/dist/cli.mjs +30 -10
  39. package/dist/{client-DGDPaN7U.mjs → client-BA-J7usH.mjs} +10 -2
  40. package/dist/{conformance-DRR23YNy.mjs → conformance-CT5EeHeC.mjs} +5 -2
  41. package/dist/conformance.d.mts +1 -1
  42. package/dist/conformance.mjs +1 -1
  43. package/dist/{engine-Bir5s8ts.mjs → engine-D0oBUBEK.mjs} +249 -163
  44. package/dist/events-CewD4kw7.mjs +75 -0
  45. package/dist/{exports-Dvc1o52I.mjs → exports-CgjfMrQ-.mjs} +35 -32
  46. package/dist/exports-ZsgjzEnM.d.mts +4 -0
  47. package/dist/external-BV_cUncs.mjs +151 -0
  48. package/dist/folds-CRYY14kc.d.mts +534 -0
  49. package/dist/{reducers-mUPFuFJV.mjs → folds-Dyjput8M.mjs} +6 -73
  50. package/dist/ids-C6AYo1bu.mjs +31 -0
  51. package/dist/index.d.mts +4 -4
  52. package/dist/{index.default-Bkg77Pxn.mjs → index.default-C5Cpz3sa.mjs} +1 -1
  53. package/dist/index.mjs +5 -5
  54. package/dist/index.node.d.mts +4 -4
  55. package/dist/index.node.mjs +3 -3
  56. package/dist/inspect/app.js +31 -31
  57. package/dist/inspect/engine.js +5 -5
  58. package/dist/internal-CvsrprnU-CRY89I-b.mjs +203 -0
  59. package/dist/{local-Cr_dSjvs.mjs → local-DhkrAqPP.mjs} +1 -1
  60. package/dist/mentions-CB6ShY4R.mjs +72 -0
  61. package/dist/{targets-yM5JBUgr.mjs → middleware-CqaCwucP.mjs} +1 -33
  62. package/dist/platform-B4TnJtWu-CwcTyKg2.mjs +32 -0
  63. package/dist/{project-CFiArraX.mjs → project-DMDST9kl.mjs} +1 -1
  64. package/dist/{provider-BGjz-mDa.d.mts → provider-B0b9Qxw4.d.mts} +170 -40
  65. package/dist/provider-B3DpYSDq.mjs +220 -0
  66. package/dist/redis-tables-DN9PfIDi.mjs +3897 -0
  67. package/dist/reducer-CYPHsSmd-BQIfozGi.d.mts +170 -0
  68. package/dist/reducer-CjmvZSAb-D_OSISmj.mjs +74 -0
  69. package/dist/{contract-CuJmlrhw.mjs → reducers-BFmIXilu.mjs} +260 -287
  70. package/dist/retryable-lazy-DZWmHpii-Bye-W8q7.mjs +19 -0
  71. package/dist/scheduler-task-X1SToDjP-BVoWuInI.mjs +69 -0
  72. package/dist/{serve-BsOOc-6_.mjs → serve-BGYw0uNL.mjs} +5 -5
  73. package/dist/{server-jwPY7_wi.mjs → server-DPxBSogx.mjs} +4 -4
  74. package/dist/server-DtFrWxi3-Boa6gdWO.mjs +3602 -0
  75. package/dist/server-qgez0Xny-D5mLfqFQ.d.mts +385 -0
  76. package/dist/{serving-BNbzc96L.mjs → serving-CNkgUPW1.mjs} +1 -1
  77. package/dist/session-clear-wHd5GCpZ-BnKSxReK.mjs +51 -0
  78. package/dist/{sql-DbiEnqM0.mjs → sql-U9WTxrwJ.mjs} +2 -2
  79. package/dist/{sqlite-db-Cbj03vmp.mjs → sqlite-db-Ds4majtT.mjs} +1 -1
  80. package/dist/store-D0MhBvg6-4Z2L0Kqd.d.mts +530 -0
  81. package/dist/store-N8PXxDAS-BWBgzBHp.mjs +5 -0
  82. package/dist/store-codec-DwrWlgQx-CYxGW0aD.mjs +61 -0
  83. package/dist/store-memory-Bas9Uq5C.mjs +872 -0
  84. package/dist/store-memory-ZLiZbH3R.mjs +2 -0
  85. package/dist/store-polling-BZt2Rf3D-BiEM6_HB.mjs +148 -0
  86. package/dist/store-presence-polling-JuAy2Ck2-CYpuK75X.mjs +109 -0
  87. package/dist/store-sqlite-BmwL3pUR.mjs +2 -0
  88. package/dist/store-sqlite-CxxPCcCT.mjs +1053 -0
  89. package/dist/stream-buffer-DrTyiIuo-BKy-vI14.mjs +53 -0
  90. package/dist/sync-github.d.mts +24 -17
  91. package/dist/sync-github.mjs +89 -22
  92. package/dist/sync-kapso.d.mts +15 -13
  93. package/dist/sync-kapso.mjs +17 -14
  94. package/dist/sync-linear.d.mts +219 -0
  95. package/dist/sync-linear.mjs +311 -0
  96. package/dist/sync-slack.d.mts +12 -17
  97. package/dist/sync-slack.mjs +54 -42
  98. package/dist/sync.chat.d.mts +1 -1
  99. package/dist/sync.chat.mjs +1 -1
  100. package/dist/sync.d.mts +3 -3
  101. package/dist/sync.mjs +5 -4
  102. package/dist/targets-C-wdR5wP.mjs +33 -0
  103. package/dist/{tokens-DKLn5P9B.mjs → tokens-DhuzKzAl.mjs} +1 -1
  104. package/dist/{types-BHCjTZdB.d.mts → types-8yq2VnKI.d.mts} +2 -2
  105. package/dist/{users-Dls7_ANV.mjs → users-yryHKFMo.mjs} +2 -2
  106. package/dist/{watch-spec-W0c7ypHJ.mjs → watchers-hash-RLtnSF-t.mjs} +233 -28
  107. package/docs/01-board/01-mounting.md +1 -1
  108. package/docs/02-sync/01-overview.md +72 -48
  109. package/docs/02-sync/02-functions.md +398 -0
  110. package/docs/02-sync/03-slack.md +30 -25
  111. package/docs/02-sync/04-github.md +62 -26
  112. package/docs/02-sync/05-whatsapp.md +23 -19
  113. package/docs/02-sync/06-providers.md +60 -27
  114. package/docs/02-sync/07-linear.md +129 -0
  115. package/docs/03-guides/04-triage.md +1 -1
  116. package/docs/04-reference/01-cli.md +26 -6
  117. package/docs/04-reference/02-board.md +18 -5
  118. package/docs/04-reference/03-http.md +5 -2
  119. package/docs/04-reference/05-conformance.md +1 -1
  120. package/docs/index.md +5 -10
  121. package/package.json +31 -6
  122. package/dist/a2-D24cvwpN.d.mts +0 -3359
  123. package/dist/access-CJU2-vaa.mjs +0 -226
  124. package/dist/attempts-Bxp9KgPf.mjs +0 -919
  125. package/dist/exports-DLiE7fLY.d.mts +0 -4
  126. package/dist/provider-CuewcDNZ.mjs +0 -205
  127. package/dist/redis-tables-B35vv4CZ.mjs +0 -842
  128. package/dist/reducers-B_vUGQRp.d.mts +0 -1172
  129. package/dist/watchers-hash-CLjsa6od.mjs +0 -225
  130. 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 };