@delma/fylo 2.0.1 → 2.1.1
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 +206 -261
- package/dist/adapters/cipher.js +155 -0
- package/dist/adapters/cipher.js.map +1 -0
- package/dist/core/collection.js +6 -0
- package/dist/core/collection.js.map +1 -0
- package/dist/core/directory.js +48 -0
- package/dist/core/directory.js.map +1 -0
- package/dist/core/doc-id.js +15 -0
- package/dist/core/doc-id.js.map +1 -0
- package/dist/core/extensions.js +16 -0
- package/dist/core/extensions.js.map +1 -0
- package/dist/core/format.js +355 -0
- package/dist/core/format.js.map +1 -0
- package/dist/core/parser.js +764 -0
- package/dist/core/parser.js.map +1 -0
- package/dist/core/query.js +47 -0
- package/dist/core/query.js.map +1 -0
- package/dist/engines/s3-files/documents.js +62 -0
- package/dist/engines/s3-files/documents.js.map +1 -0
- package/dist/engines/s3-files/filesystem.js +165 -0
- package/dist/engines/s3-files/filesystem.js.map +1 -0
- package/dist/engines/s3-files/query.js +235 -0
- package/dist/engines/s3-files/query.js.map +1 -0
- package/dist/engines/s3-files/types.js +2 -0
- package/dist/engines/s3-files/types.js.map +1 -0
- package/dist/engines/s3-files.js +629 -0
- package/dist/engines/s3-files.js.map +1 -0
- package/dist/engines/types.js +2 -0
- package/dist/engines/types.js.map +1 -0
- package/dist/index.js +562 -0
- package/dist/index.js.map +1 -0
- package/dist/sync.js +18 -0
- package/dist/sync.js.map +1 -0
- package/dist/types/fylo.d.ts +179 -0
- package/{src → dist}/types/node-runtime.d.ts +1 -0
- package/package.json +3 -6
- package/.env.example +0 -16
- package/.github/copilot-instructions.md +0 -3
- package/.github/prompts/release.prompt.md +0 -10
- package/.github/workflows/ci.yml +0 -37
- package/.github/workflows/publish.yml +0 -91
- package/.prettierrc +0 -7
- package/AGENTS.md +0 -3
- package/CLAUDE.md +0 -3
- package/eslint.config.js +0 -32
- package/src/CLI +0 -39
- package/src/adapters/cipher.ts +0 -180
- package/src/adapters/redis.ts +0 -487
- package/src/adapters/s3.ts +0 -61
- package/src/core/collection.ts +0 -5
- package/src/core/directory.ts +0 -387
- package/src/core/extensions.ts +0 -21
- package/src/core/format.ts +0 -457
- package/src/core/parser.ts +0 -901
- package/src/core/query.ts +0 -53
- package/src/core/walker.ts +0 -174
- package/src/core/write-queue.ts +0 -59
- package/src/engines/s3-files.ts +0 -1068
- package/src/engines/types.ts +0 -21
- package/src/index.ts +0 -1727
- package/src/migrate-cli.ts +0 -22
- package/src/migrate.ts +0 -74
- package/src/types/fylo.d.ts +0 -261
- package/src/types/write-queue.ts +0 -42
- package/src/worker.ts +0 -18
- package/src/workers/write-worker.ts +0 -120
- package/tests/collection/truncate.test.js +0 -35
- package/tests/data.js +0 -97
- package/tests/index.js +0 -14
- package/tests/integration/aws-s3-files.canary.test.js +0 -22
- package/tests/integration/create.test.js +0 -39
- package/tests/integration/delete.test.js +0 -95
- package/tests/integration/edge-cases.test.js +0 -158
- package/tests/integration/encryption.test.js +0 -131
- package/tests/integration/export.test.js +0 -46
- package/tests/integration/join-modes.test.js +0 -154
- package/tests/integration/migration.test.js +0 -38
- package/tests/integration/nested.test.js +0 -142
- package/tests/integration/operators.test.js +0 -122
- package/tests/integration/queue.test.js +0 -83
- package/tests/integration/read.test.js +0 -119
- package/tests/integration/rollback.test.js +0 -60
- package/tests/integration/s3-files.test.js +0 -192
- package/tests/integration/update.test.js +0 -99
- package/tests/mocks/cipher.js +0 -40
- package/tests/mocks/redis.js +0 -123
- package/tests/mocks/s3.js +0 -80
- package/tests/schemas/album.d.ts +0 -5
- package/tests/schemas/album.json +0 -5
- package/tests/schemas/comment.d.ts +0 -7
- package/tests/schemas/comment.json +0 -7
- package/tests/schemas/photo.d.ts +0 -7
- package/tests/schemas/photo.json +0 -7
- package/tests/schemas/post.d.ts +0 -6
- package/tests/schemas/post.json +0 -6
- package/tests/schemas/tip.d.ts +0 -7
- package/tests/schemas/tip.json +0 -7
- package/tests/schemas/todo.d.ts +0 -6
- package/tests/schemas/todo.json +0 -6
- package/tests/schemas/user.d.ts +0 -23
- package/tests/schemas/user.json +0 -23
- package/tsconfig.json +0 -21
- package/tsconfig.typecheck.json +0 -31
- /package/{src → dist}/types/bun-runtime.d.ts +0 -0
- /package/{src → dist}/types/index.d.ts +0 -0
- /package/{src → dist}/types/query.d.ts +0 -0
- /package/{src → dist}/types/vendor-modules.d.ts +0 -0
package/src/adapters/redis.ts
DELETED
|
@@ -1,487 +0,0 @@
|
|
|
1
|
-
import { RedisClient } from 'bun'
|
|
2
|
-
import { S3 } from './s3'
|
|
3
|
-
import type {
|
|
4
|
-
DeadLetterJob,
|
|
5
|
-
QueueStats,
|
|
6
|
-
StreamJobEntry,
|
|
7
|
-
WriteJob,
|
|
8
|
-
WriteJobStatus
|
|
9
|
-
} from '../types/write-queue'
|
|
10
|
-
|
|
11
|
-
export class Redis {
|
|
12
|
-
static readonly WRITE_STREAM = 'fylo:writes'
|
|
13
|
-
|
|
14
|
-
static readonly WRITE_GROUP = 'fylo-workers'
|
|
15
|
-
|
|
16
|
-
static readonly DEAD_LETTER_STREAM = 'fylo:writes:dead'
|
|
17
|
-
|
|
18
|
-
private client: RedisClient
|
|
19
|
-
|
|
20
|
-
private static LOGGING = process.env.LOGGING
|
|
21
|
-
|
|
22
|
-
constructor() {
|
|
23
|
-
const redisUrl = process.env.REDIS_URL
|
|
24
|
-
if (!redisUrl) throw new Error('REDIS_URL environment variable is required')
|
|
25
|
-
|
|
26
|
-
this.client = new RedisClient(redisUrl, {
|
|
27
|
-
connectionTimeout: process.env.REDIS_CONN_TIMEOUT
|
|
28
|
-
? Number(process.env.REDIS_CONN_TIMEOUT)
|
|
29
|
-
: undefined,
|
|
30
|
-
idleTimeout: process.env.REDIS_IDLE_TIMEOUT
|
|
31
|
-
? Number(process.env.REDIS_IDLE_TIMEOUT)
|
|
32
|
-
: undefined,
|
|
33
|
-
autoReconnect: process.env.REDIS_AUTO_CONNECT ? true : undefined,
|
|
34
|
-
maxRetries: process.env.REDIS_MAX_RETRIES
|
|
35
|
-
? Number(process.env.REDIS_MAX_RETRIES)
|
|
36
|
-
: undefined,
|
|
37
|
-
enableOfflineQueue: process.env.REDIS_ENABLE_OFFLINE_QUEUE ? true : undefined,
|
|
38
|
-
enableAutoPipelining: process.env.REDIS_ENABLE_AUTO_PIPELINING ? true : undefined,
|
|
39
|
-
tls: process.env.REDIS_TLS ? true : undefined
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
this.client.onconnect = () => {
|
|
43
|
-
if (Redis.LOGGING) console.log('Client Connected')
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
this.client.onclose = (err) => console.error('Redis client connection closed', err.message)
|
|
47
|
-
|
|
48
|
-
this.client.connect()
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
private async ensureWriteGroup() {
|
|
52
|
-
if (!this.client.connected) throw new Error('Redis not connected!')
|
|
53
|
-
|
|
54
|
-
try {
|
|
55
|
-
await this.client.send('XGROUP', [
|
|
56
|
-
'CREATE',
|
|
57
|
-
Redis.WRITE_STREAM,
|
|
58
|
-
Redis.WRITE_GROUP,
|
|
59
|
-
'$',
|
|
60
|
-
'MKSTREAM'
|
|
61
|
-
])
|
|
62
|
-
} catch (err) {
|
|
63
|
-
if (!(err instanceof Error) || !err.message.includes('BUSYGROUP')) throw err
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
private static hashKey(jobId: string) {
|
|
68
|
-
return `fylo:job:${jobId}`
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
private static docKey(collection: string, docId: _ttid) {
|
|
72
|
-
return `fylo:doc:${collection}:${docId}`
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
private static lockKey(collection: string, docId: _ttid) {
|
|
76
|
-
return `fylo:lock:${collection}:${docId}`
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
private static parseHash(values: unknown): Record<string, string> {
|
|
80
|
-
if (!Array.isArray(values)) return {}
|
|
81
|
-
|
|
82
|
-
const parsed: Record<string, string> = {}
|
|
83
|
-
|
|
84
|
-
for (let i = 0; i < values.length; i += 2) {
|
|
85
|
-
parsed[String(values[i])] = String(values[i + 1] ?? '')
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
return parsed
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
async publish(collection: string, action: 'insert' | 'delete', keyId: string | _ttid) {
|
|
92
|
-
if (this.client.connected) {
|
|
93
|
-
await this.client.publish(
|
|
94
|
-
S3.getBucketFormat(collection),
|
|
95
|
-
JSON.stringify({ action, keyId })
|
|
96
|
-
)
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
async claimTTID(_id: _ttid, ttlSeconds: number = 10): Promise<boolean> {
|
|
101
|
-
if (!this.client.connected) return false
|
|
102
|
-
|
|
103
|
-
const result = await this.client.send('SET', [
|
|
104
|
-
`ttid:${_id}`,
|
|
105
|
-
'1',
|
|
106
|
-
'NX',
|
|
107
|
-
'EX',
|
|
108
|
-
String(ttlSeconds)
|
|
109
|
-
])
|
|
110
|
-
|
|
111
|
-
return result === 'OK'
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
async enqueueWrite<T extends Record<string, any>>(job: WriteJob<T>) {
|
|
115
|
-
if (!this.client.connected) throw new Error('Redis not connected!')
|
|
116
|
-
|
|
117
|
-
await this.ensureWriteGroup()
|
|
118
|
-
|
|
119
|
-
const now = Date.now()
|
|
120
|
-
const payload = JSON.stringify(job.payload)
|
|
121
|
-
|
|
122
|
-
await this.client.send('HSET', [
|
|
123
|
-
Redis.hashKey(job.jobId),
|
|
124
|
-
'jobId',
|
|
125
|
-
job.jobId,
|
|
126
|
-
'collection',
|
|
127
|
-
job.collection,
|
|
128
|
-
'docId',
|
|
129
|
-
job.docId,
|
|
130
|
-
'operation',
|
|
131
|
-
job.operation,
|
|
132
|
-
'payload',
|
|
133
|
-
payload,
|
|
134
|
-
'status',
|
|
135
|
-
job.status,
|
|
136
|
-
'attempts',
|
|
137
|
-
String(job.attempts),
|
|
138
|
-
'createdAt',
|
|
139
|
-
String(job.createdAt),
|
|
140
|
-
'updatedAt',
|
|
141
|
-
String(now),
|
|
142
|
-
'nextAttemptAt',
|
|
143
|
-
String(job.nextAttemptAt ?? now)
|
|
144
|
-
])
|
|
145
|
-
|
|
146
|
-
await this.client.send('HSET', [
|
|
147
|
-
Redis.docKey(job.collection, job.docId),
|
|
148
|
-
'status',
|
|
149
|
-
'queued',
|
|
150
|
-
'lastJobId',
|
|
151
|
-
job.jobId,
|
|
152
|
-
'updatedAt',
|
|
153
|
-
String(now)
|
|
154
|
-
])
|
|
155
|
-
|
|
156
|
-
return await this.client.send('XADD', [
|
|
157
|
-
Redis.WRITE_STREAM,
|
|
158
|
-
'*',
|
|
159
|
-
'jobId',
|
|
160
|
-
job.jobId,
|
|
161
|
-
'collection',
|
|
162
|
-
job.collection,
|
|
163
|
-
'docId',
|
|
164
|
-
job.docId,
|
|
165
|
-
'operation',
|
|
166
|
-
job.operation
|
|
167
|
-
])
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
async readWriteJobs(
|
|
171
|
-
workerId: string,
|
|
172
|
-
count: number = 1,
|
|
173
|
-
blockMs: number = 1000
|
|
174
|
-
): Promise<Array<StreamJobEntry>> {
|
|
175
|
-
if (!this.client.connected) throw new Error('Redis not connected!')
|
|
176
|
-
|
|
177
|
-
await this.ensureWriteGroup()
|
|
178
|
-
|
|
179
|
-
const rows = await this.client.send('XREADGROUP', [
|
|
180
|
-
'GROUP',
|
|
181
|
-
Redis.WRITE_GROUP,
|
|
182
|
-
workerId,
|
|
183
|
-
'COUNT',
|
|
184
|
-
String(count),
|
|
185
|
-
'BLOCK',
|
|
186
|
-
String(blockMs),
|
|
187
|
-
'STREAMS',
|
|
188
|
-
Redis.WRITE_STREAM,
|
|
189
|
-
'>'
|
|
190
|
-
])
|
|
191
|
-
|
|
192
|
-
if (!Array.isArray(rows) || rows.length === 0) return []
|
|
193
|
-
|
|
194
|
-
const items: Array<StreamJobEntry> = []
|
|
195
|
-
|
|
196
|
-
for (const streamRow of rows as unknown[]) {
|
|
197
|
-
if (!Array.isArray(streamRow) || streamRow.length < 2) continue
|
|
198
|
-
const entries = streamRow[1]
|
|
199
|
-
if (!Array.isArray(entries)) continue
|
|
200
|
-
|
|
201
|
-
for (const entry of entries as unknown[]) {
|
|
202
|
-
if (!Array.isArray(entry) || entry.length < 2) continue
|
|
203
|
-
const streamId = String(entry[0])
|
|
204
|
-
const fields = Redis.parseHash(entry[1])
|
|
205
|
-
const job = await this.getJob(fields.jobId)
|
|
206
|
-
if (job) items.push({ streamId, job })
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
return items
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
async ackWriteJob(streamId: string) {
|
|
214
|
-
if (!this.client.connected) throw new Error('Redis not connected!')
|
|
215
|
-
|
|
216
|
-
await this.client.send('XACK', [Redis.WRITE_STREAM, Redis.WRITE_GROUP, streamId])
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
async deadLetterWriteJob(streamId: string, job: WriteJob, reason?: string) {
|
|
220
|
-
if (!this.client.connected) throw new Error('Redis not connected!')
|
|
221
|
-
|
|
222
|
-
const failedAt = Date.now()
|
|
223
|
-
|
|
224
|
-
await this.client.send('XADD', [
|
|
225
|
-
Redis.DEAD_LETTER_STREAM,
|
|
226
|
-
'*',
|
|
227
|
-
'jobId',
|
|
228
|
-
job.jobId,
|
|
229
|
-
'collection',
|
|
230
|
-
job.collection,
|
|
231
|
-
'docId',
|
|
232
|
-
job.docId,
|
|
233
|
-
'operation',
|
|
234
|
-
job.operation,
|
|
235
|
-
'reason',
|
|
236
|
-
reason ?? '',
|
|
237
|
-
'failedAt',
|
|
238
|
-
String(failedAt)
|
|
239
|
-
])
|
|
240
|
-
|
|
241
|
-
await this.ackWriteJob(streamId)
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
async claimPendingJobs(
|
|
245
|
-
workerId: string,
|
|
246
|
-
minIdleMs: number = 30_000,
|
|
247
|
-
count: number = 10
|
|
248
|
-
): Promise<Array<StreamJobEntry>> {
|
|
249
|
-
if (!this.client.connected) throw new Error('Redis not connected!')
|
|
250
|
-
|
|
251
|
-
await this.ensureWriteGroup()
|
|
252
|
-
|
|
253
|
-
const result = await this.client.send('XAUTOCLAIM', [
|
|
254
|
-
Redis.WRITE_STREAM,
|
|
255
|
-
Redis.WRITE_GROUP,
|
|
256
|
-
workerId,
|
|
257
|
-
String(minIdleMs),
|
|
258
|
-
'0-0',
|
|
259
|
-
'COUNT',
|
|
260
|
-
String(count)
|
|
261
|
-
])
|
|
262
|
-
|
|
263
|
-
if (!Array.isArray(result) || result.length < 2 || !Array.isArray(result[1])) return []
|
|
264
|
-
|
|
265
|
-
const items: Array<StreamJobEntry> = []
|
|
266
|
-
|
|
267
|
-
for (const entry of result[1] as unknown[]) {
|
|
268
|
-
if (!Array.isArray(entry) || entry.length < 2) continue
|
|
269
|
-
const streamId = String(entry[0])
|
|
270
|
-
const fields = Redis.parseHash(entry[1])
|
|
271
|
-
const job = await this.getJob(fields.jobId)
|
|
272
|
-
if (job) items.push({ streamId, job })
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
return items
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
async setJobStatus(
|
|
279
|
-
jobId: string,
|
|
280
|
-
status: WriteJobStatus,
|
|
281
|
-
extra: Partial<Pick<WriteJob, 'workerId' | 'error' | 'attempts' | 'nextAttemptAt'>> = {}
|
|
282
|
-
) {
|
|
283
|
-
if (!this.client.connected) throw new Error('Redis not connected!')
|
|
284
|
-
|
|
285
|
-
const args = [Redis.hashKey(jobId), 'status', status, 'updatedAt', String(Date.now())]
|
|
286
|
-
|
|
287
|
-
if (extra.workerId) args.push('workerId', extra.workerId)
|
|
288
|
-
if (extra.error) args.push('error', extra.error)
|
|
289
|
-
if (typeof extra.attempts === 'number') args.push('attempts', String(extra.attempts))
|
|
290
|
-
if (typeof extra.nextAttemptAt === 'number')
|
|
291
|
-
args.push('nextAttemptAt', String(extra.nextAttemptAt))
|
|
292
|
-
|
|
293
|
-
await this.client.send('HSET', args)
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
async setDocStatus(collection: string, docId: _ttid, status: WriteJobStatus, jobId?: string) {
|
|
297
|
-
if (!this.client.connected) throw new Error('Redis not connected!')
|
|
298
|
-
|
|
299
|
-
const args = [
|
|
300
|
-
Redis.docKey(collection, docId),
|
|
301
|
-
'status',
|
|
302
|
-
status,
|
|
303
|
-
'updatedAt',
|
|
304
|
-
String(Date.now())
|
|
305
|
-
]
|
|
306
|
-
|
|
307
|
-
if (jobId) args.push('lastJobId', jobId)
|
|
308
|
-
|
|
309
|
-
await this.client.send('HSET', args)
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
async getJob(jobId: string): Promise<WriteJob | null> {
|
|
313
|
-
if (!this.client.connected) throw new Error('Redis not connected!')
|
|
314
|
-
|
|
315
|
-
const hash = Redis.parseHash(await this.client.send('HGETALL', [Redis.hashKey(jobId)]))
|
|
316
|
-
|
|
317
|
-
if (Object.keys(hash).length === 0) return null
|
|
318
|
-
|
|
319
|
-
return {
|
|
320
|
-
jobId: hash.jobId,
|
|
321
|
-
collection: hash.collection,
|
|
322
|
-
docId: hash.docId as _ttid,
|
|
323
|
-
operation: hash.operation as WriteJob['operation'],
|
|
324
|
-
payload: JSON.parse(hash.payload),
|
|
325
|
-
status: hash.status as WriteJobStatus,
|
|
326
|
-
attempts: Number(hash.attempts ?? 0),
|
|
327
|
-
createdAt: Number(hash.createdAt ?? 0),
|
|
328
|
-
updatedAt: Number(hash.updatedAt ?? 0),
|
|
329
|
-
nextAttemptAt: Number(hash.nextAttemptAt ?? 0) || undefined,
|
|
330
|
-
workerId: hash.workerId || undefined,
|
|
331
|
-
error: hash.error || undefined
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
async getDocStatus(collection: string, docId: _ttid) {
|
|
336
|
-
if (!this.client.connected) throw new Error('Redis not connected!')
|
|
337
|
-
|
|
338
|
-
const hash = Redis.parseHash(
|
|
339
|
-
await this.client.send('HGETALL', [Redis.docKey(collection, docId)])
|
|
340
|
-
)
|
|
341
|
-
|
|
342
|
-
return Object.keys(hash).length > 0 ? hash : null
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
async readDeadLetters(count: number = 10): Promise<Array<DeadLetterJob>> {
|
|
346
|
-
if (!this.client.connected) throw new Error('Redis not connected!')
|
|
347
|
-
|
|
348
|
-
const rows = await this.client.send('XRANGE', [
|
|
349
|
-
Redis.DEAD_LETTER_STREAM,
|
|
350
|
-
'-',
|
|
351
|
-
'+',
|
|
352
|
-
'COUNT',
|
|
353
|
-
String(count)
|
|
354
|
-
])
|
|
355
|
-
|
|
356
|
-
if (!Array.isArray(rows)) return []
|
|
357
|
-
|
|
358
|
-
const items: Array<DeadLetterJob> = []
|
|
359
|
-
|
|
360
|
-
for (const row of rows as unknown[]) {
|
|
361
|
-
if (!Array.isArray(row) || row.length < 2) continue
|
|
362
|
-
const streamId = String(row[0])
|
|
363
|
-
const fields = Redis.parseHash(row[1])
|
|
364
|
-
const job = await this.getJob(fields.jobId)
|
|
365
|
-
|
|
366
|
-
if (job) {
|
|
367
|
-
items.push({
|
|
368
|
-
streamId,
|
|
369
|
-
job,
|
|
370
|
-
reason: fields.reason || undefined,
|
|
371
|
-
failedAt: Number(fields.failedAt ?? 0)
|
|
372
|
-
})
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
return items
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
async replayDeadLetter(streamId: string): Promise<WriteJob | null> {
|
|
380
|
-
if (!this.client.connected) throw new Error('Redis not connected!')
|
|
381
|
-
|
|
382
|
-
const rows = await this.client.send('XRANGE', [
|
|
383
|
-
Redis.DEAD_LETTER_STREAM,
|
|
384
|
-
streamId,
|
|
385
|
-
streamId,
|
|
386
|
-
'COUNT',
|
|
387
|
-
'1'
|
|
388
|
-
])
|
|
389
|
-
|
|
390
|
-
if (!Array.isArray(rows) || rows.length === 0) return null
|
|
391
|
-
|
|
392
|
-
const row = rows[0]
|
|
393
|
-
if (!Array.isArray(row) || row.length < 2) return null
|
|
394
|
-
|
|
395
|
-
const fields = Redis.parseHash(row[1])
|
|
396
|
-
const job = await this.getJob(fields.jobId)
|
|
397
|
-
|
|
398
|
-
if (!job) return null
|
|
399
|
-
|
|
400
|
-
const replayed: WriteJob = {
|
|
401
|
-
...job,
|
|
402
|
-
status: 'queued',
|
|
403
|
-
error: undefined,
|
|
404
|
-
workerId: undefined,
|
|
405
|
-
attempts: 0,
|
|
406
|
-
updatedAt: Date.now(),
|
|
407
|
-
nextAttemptAt: Date.now()
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
await this.enqueueWrite(replayed)
|
|
411
|
-
await this.client.send('XDEL', [Redis.DEAD_LETTER_STREAM, streamId])
|
|
412
|
-
|
|
413
|
-
return replayed
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
async getQueueStats(): Promise<QueueStats> {
|
|
417
|
-
if (!this.client.connected) throw new Error('Redis not connected!')
|
|
418
|
-
|
|
419
|
-
await this.ensureWriteGroup()
|
|
420
|
-
|
|
421
|
-
const [queuedRaw, deadRaw, pendingRaw] = await Promise.all([
|
|
422
|
-
this.client.send('XLEN', [Redis.WRITE_STREAM]),
|
|
423
|
-
this.client.send('XLEN', [Redis.DEAD_LETTER_STREAM]),
|
|
424
|
-
this.client.send('XPENDING', [Redis.WRITE_STREAM, Redis.WRITE_GROUP])
|
|
425
|
-
])
|
|
426
|
-
|
|
427
|
-
const pending = Array.isArray(pendingRaw) ? Number(pendingRaw[0] ?? 0) : 0
|
|
428
|
-
|
|
429
|
-
return {
|
|
430
|
-
queued: Number(queuedRaw ?? 0),
|
|
431
|
-
pending,
|
|
432
|
-
deadLetters: Number(deadRaw ?? 0)
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
async acquireDocLock(collection: string, docId: _ttid, jobId: string, ttlSeconds: number = 60) {
|
|
437
|
-
if (!this.client.connected) throw new Error('Redis not connected!')
|
|
438
|
-
|
|
439
|
-
const result = await this.client.send('SET', [
|
|
440
|
-
Redis.lockKey(collection, docId),
|
|
441
|
-
jobId,
|
|
442
|
-
'NX',
|
|
443
|
-
'EX',
|
|
444
|
-
String(ttlSeconds)
|
|
445
|
-
])
|
|
446
|
-
|
|
447
|
-
return result === 'OK'
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
async releaseDocLock(collection: string, docId: _ttid, jobId: string) {
|
|
451
|
-
if (!this.client.connected) throw new Error('Redis not connected!')
|
|
452
|
-
|
|
453
|
-
const key = Redis.lockKey(collection, docId)
|
|
454
|
-
const current = await this.client.send('GET', [key])
|
|
455
|
-
if (current === jobId) await this.client.send('DEL', [key])
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
async *subscribe(collection: string) {
|
|
459
|
-
if (!this.client.connected) throw new Error('Redis not connected!')
|
|
460
|
-
|
|
461
|
-
const client = this.client
|
|
462
|
-
|
|
463
|
-
const stream = new ReadableStream({
|
|
464
|
-
async start(controller) {
|
|
465
|
-
await client.subscribe(S3.getBucketFormat(collection), (message) => {
|
|
466
|
-
controller.enqueue(message)
|
|
467
|
-
})
|
|
468
|
-
}
|
|
469
|
-
})
|
|
470
|
-
|
|
471
|
-
const reader = stream.getReader()
|
|
472
|
-
|
|
473
|
-
while (true) {
|
|
474
|
-
const { done, value } = await reader.read()
|
|
475
|
-
if (done) break
|
|
476
|
-
const parsed = JSON.parse(value)
|
|
477
|
-
if (
|
|
478
|
-
typeof parsed !== 'object' ||
|
|
479
|
-
parsed === null ||
|
|
480
|
-
!('action' in parsed) ||
|
|
481
|
-
!('keyId' in parsed)
|
|
482
|
-
)
|
|
483
|
-
continue
|
|
484
|
-
yield parsed
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
}
|
package/src/adapters/s3.ts
DELETED
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
import { $, S3Client } from 'bun'
|
|
2
|
-
import { validateCollectionName } from '../core/collection'
|
|
3
|
-
|
|
4
|
-
export class S3 {
|
|
5
|
-
static readonly BUCKET_ENV = process.env.BUCKET_PREFIX
|
|
6
|
-
|
|
7
|
-
static readonly CREDS = {
|
|
8
|
-
accessKeyId: process.env.S3_ACCESS_KEY_ID ?? process.env.AWS_ACCESS_KEY_ID,
|
|
9
|
-
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY ?? process.env.AWS_SECRET_ACCESS_KEY,
|
|
10
|
-
region: process.env.S3_REGION ?? process.env.AWS_REGION,
|
|
11
|
-
endpoint: process.env.S3_ENDPOINT ?? process.env.AWS_ENDPOINT
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
private static validateCollection(collection: string): void {
|
|
15
|
-
validateCollectionName(collection)
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
static getBucketFormat(collection: string) {
|
|
19
|
-
S3.validateCollection(collection)
|
|
20
|
-
return S3.BUCKET_ENV ? `${S3.BUCKET_ENV}-${collection}` : collection
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
static file(collection: string, path: string) {
|
|
24
|
-
return S3Client.file(path, {
|
|
25
|
-
bucket: S3.getBucketFormat(collection),
|
|
26
|
-
...S3.CREDS
|
|
27
|
-
})
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
static async list(collection: string, options?: Bun.S3ListObjectsOptions) {
|
|
31
|
-
return await S3Client.list(options, {
|
|
32
|
-
bucket: S3.getBucketFormat(collection),
|
|
33
|
-
...S3.CREDS
|
|
34
|
-
})
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
static async put(collection: string, path: string, data: string) {
|
|
38
|
-
await S3Client.write(path, data, {
|
|
39
|
-
bucket: S3.getBucketFormat(collection),
|
|
40
|
-
...S3.CREDS
|
|
41
|
-
})
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
static async delete(collection: string, path: string) {
|
|
45
|
-
await S3Client.delete(path, {
|
|
46
|
-
bucket: S3.getBucketFormat(collection),
|
|
47
|
-
...S3.CREDS
|
|
48
|
-
})
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
static async createBucket(collection: string) {
|
|
52
|
-
const endpoint = S3.CREDS.endpoint
|
|
53
|
-
await $`aws s3 mb s3://${S3.getBucketFormat(collection)} ${endpoint ? `--endpoint-url=${endpoint}` : ''}`.quiet()
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
static async deleteBucket(collection: string) {
|
|
57
|
-
const endpoint = S3.CREDS.endpoint
|
|
58
|
-
await $`aws s3 rm s3://${S3.getBucketFormat(collection)} --recursive ${endpoint ? `--endpoint-url=${endpoint}` : ''}`.quiet()
|
|
59
|
-
await $`aws s3 rb s3://${S3.getBucketFormat(collection)} ${endpoint ? `--endpoint-url=${endpoint}` : ''}`.quiet()
|
|
60
|
-
}
|
|
61
|
-
}
|