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