@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.
Files changed (66) hide show
  1. package/README.md +141 -62
  2. package/eslint.config.js +8 -4
  3. package/package.json +9 -7
  4. package/src/CLI +16 -14
  5. package/src/adapters/cipher.ts +12 -6
  6. package/src/adapters/redis.ts +193 -123
  7. package/src/adapters/s3.ts +6 -12
  8. package/src/core/collection.ts +5 -0
  9. package/src/core/directory.ts +120 -151
  10. package/src/core/extensions.ts +4 -2
  11. package/src/core/format.ts +390 -419
  12. package/src/core/parser.ts +167 -142
  13. package/src/core/query.ts +31 -26
  14. package/src/core/walker.ts +68 -61
  15. package/src/core/write-queue.ts +7 -4
  16. package/src/engines/s3-files.ts +888 -0
  17. package/src/engines/types.ts +21 -0
  18. package/src/index.ts +754 -378
  19. package/src/migrate-cli.ts +22 -0
  20. package/src/migrate.ts +74 -0
  21. package/src/types/bun-runtime.d.ts +73 -0
  22. package/src/types/fylo.d.ts +115 -27
  23. package/src/types/node-runtime.d.ts +61 -0
  24. package/src/types/query.d.ts +6 -2
  25. package/src/types/vendor-modules.d.ts +8 -7
  26. package/src/worker.ts +7 -1
  27. package/src/workers/write-worker.ts +25 -24
  28. package/tests/collection/truncate.test.js +35 -0
  29. package/tests/{data.ts → data.js} +8 -21
  30. package/tests/{index.ts → index.js} +4 -9
  31. package/tests/integration/aws-s3-files.canary.test.js +22 -0
  32. package/tests/integration/{create.test.ts → create.test.js} +13 -31
  33. package/tests/integration/delete.test.js +95 -0
  34. package/tests/integration/{edge-cases.test.ts → edge-cases.test.js} +50 -124
  35. package/tests/integration/{encryption.test.ts → encryption.test.js} +20 -65
  36. package/tests/integration/{export.test.ts → export.test.js} +8 -23
  37. package/tests/integration/{join-modes.test.ts → join-modes.test.js} +37 -104
  38. package/tests/integration/migration.test.js +38 -0
  39. package/tests/integration/nested.test.js +142 -0
  40. package/tests/integration/operators.test.js +122 -0
  41. package/tests/integration/{queue.test.ts → queue.test.js} +24 -40
  42. package/tests/integration/read.test.js +119 -0
  43. package/tests/integration/rollback.test.js +60 -0
  44. package/tests/integration/s3-files.test.js +108 -0
  45. package/tests/integration/update.test.js +99 -0
  46. package/tests/mocks/{cipher.ts → cipher.js} +11 -26
  47. package/tests/mocks/redis.js +123 -0
  48. package/tests/mocks/{s3.ts → s3.js} +24 -58
  49. package/tests/schemas/album.json +1 -1
  50. package/tests/schemas/comment.json +1 -1
  51. package/tests/schemas/photo.json +1 -1
  52. package/tests/schemas/post.json +1 -1
  53. package/tests/schemas/tip.json +1 -1
  54. package/tests/schemas/todo.json +1 -1
  55. package/tests/schemas/user.d.ts +12 -12
  56. package/tests/schemas/user.json +1 -1
  57. package/tsconfig.json +4 -2
  58. package/tsconfig.typecheck.json +31 -0
  59. package/tests/collection/truncate.test.ts +0 -56
  60. package/tests/integration/delete.test.ts +0 -147
  61. package/tests/integration/nested.test.ts +0 -212
  62. package/tests/integration/operators.test.ts +0 -167
  63. package/tests/integration/read.test.ts +0 -203
  64. package/tests/integration/rollback.test.ts +0 -105
  65. package/tests/integration/update.test.ts +0 -130
  66. package/tests/mocks/redis.ts +0 -169
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env bun
2
+ import { migrateLegacyS3ToS3Files } from './migrate'
3
+
4
+ const [, , ...args] = process.argv
5
+
6
+ const collections = args.filter((arg) => !arg.startsWith('--'))
7
+ const recreateCollections = !args.includes('--keep-existing')
8
+ const verify = !args.includes('--no-verify')
9
+
10
+ if (collections.length === 0) {
11
+ throw new Error(
12
+ 'Usage: fylo.migrate <collection> [collection...] [--keep-existing] [--no-verify]'
13
+ )
14
+ }
15
+
16
+ const summary = await migrateLegacyS3ToS3Files({
17
+ collections,
18
+ recreateCollections,
19
+ verify
20
+ })
21
+
22
+ console.log(JSON.stringify(summary, null, 2))
package/src/migrate.ts ADDED
@@ -0,0 +1,74 @@
1
+ import Fylo from './index'
2
+ import { S3FilesEngine } from './engines/s3-files'
3
+
4
+ type MigrateOptions = {
5
+ collections: string[]
6
+ s3FilesRoot?: string
7
+ recreateCollections?: boolean
8
+ verify?: boolean
9
+ }
10
+
11
+ function normalize<T>(value: T): T {
12
+ if (Array.isArray(value)) return value.map((item) => normalize(item)).sort() as T
13
+ if (value && typeof value === 'object') {
14
+ return Object.keys(value as Record<string, unknown>)
15
+ .sort()
16
+ .reduce(
17
+ (acc, key) => {
18
+ acc[key] = normalize((value as Record<string, unknown>)[key])
19
+ return acc
20
+ },
21
+ {} as Record<string, unknown>
22
+ ) as T
23
+ }
24
+ return value
25
+ }
26
+
27
+ export async function migrateLegacyS3ToS3Files({
28
+ collections,
29
+ s3FilesRoot = process.env.FYLO_S3FILES_ROOT,
30
+ recreateCollections = true,
31
+ verify = true
32
+ }: MigrateOptions) {
33
+ if (!s3FilesRoot) throw new Error('s3FilesRoot is required')
34
+
35
+ const source = new Fylo({ engine: 'legacy-s3' })
36
+ const target = new S3FilesEngine(s3FilesRoot)
37
+
38
+ const summary: Record<string, { migrated: number; verified: boolean }> = {}
39
+
40
+ for (const collection of collections) {
41
+ if (recreateCollections) {
42
+ await target.dropCollection(collection)
43
+ await target.createCollection(collection)
44
+ } else if (!(await target.hasCollection(collection))) {
45
+ await target.createCollection(collection)
46
+ }
47
+
48
+ const docs = (await source.executeSQL<Record<string, any>>(
49
+ `SELECT * FROM ${collection}`
50
+ )) as Record<_ttid, Record<string, any>>
51
+
52
+ for (const [docId, doc] of Object.entries(docs) as Array<[_ttid, Record<string, any>]>) {
53
+ await target.putDocument(collection, docId, doc)
54
+ }
55
+
56
+ let verified = false
57
+
58
+ if (verify) {
59
+ const targetFylo = new Fylo({ engine: 's3-files', s3FilesRoot })
60
+ const migratedDocs = (await targetFylo.executeSQL<Record<string, any>>(
61
+ `SELECT * FROM ${collection}`
62
+ )) as Record<_ttid, Record<string, any>>
63
+ verified = JSON.stringify(normalize(docs)) === JSON.stringify(normalize(migratedDocs))
64
+ if (!verified) throw new Error(`Verification failed for ${collection}`)
65
+ }
66
+
67
+ summary[collection] = {
68
+ migrated: Object.keys(docs).length,
69
+ verified
70
+ }
71
+ }
72
+
73
+ return summary
74
+ }
@@ -0,0 +1,73 @@
1
+ declare namespace Bun {
2
+ function sleep(ms: number): Promise<void>
3
+ function randomUUIDv7(): string
4
+
5
+ class Glob {
6
+ constructor(pattern: string)
7
+ match(input: string): boolean
8
+ }
9
+
10
+ namespace JSONL {
11
+ function parseChunk(input: Uint8Array): { values: unknown[]; read: number }
12
+ }
13
+
14
+ interface S3ListObjectsOptions {
15
+ delimiter?: string
16
+ prefix?: string
17
+ maxKeys?: number
18
+ continuationToken?: string
19
+ }
20
+
21
+ interface S3ListObjectsResponse {
22
+ commonPrefixes?: Array<{ prefix?: string }>
23
+ contents?: Array<{ key?: string }>
24
+ isTruncated?: boolean
25
+ nextContinuationToken?: string
26
+ }
27
+ }
28
+
29
+ declare module 'bun' {
30
+ export const $: (
31
+ strings: TemplateStringsArray,
32
+ ...values: any[]
33
+ ) => {
34
+ quiet(): Promise<unknown>
35
+ }
36
+
37
+ export class S3Client {
38
+ static file(path: string, options: Record<string, any>): any
39
+ static list(
40
+ options: Bun.S3ListObjectsOptions | undefined,
41
+ config: Record<string, any>
42
+ ): Promise<Bun.S3ListObjectsResponse>
43
+ static write(path: string, data: string, config: Record<string, any>): Promise<void>
44
+ static delete(path: string, config: Record<string, any>): Promise<void>
45
+ }
46
+
47
+ export class RedisClient {
48
+ connected: boolean
49
+ onconnect?: () => void
50
+ onclose?: (err: Error) => void
51
+
52
+ constructor(url: string, options?: Record<string, any>)
53
+
54
+ connect(): void
55
+ send(command: string, args: string[]): Promise<any>
56
+ publish(channel: string, message: string): Promise<unknown>
57
+ subscribe(channel: string, listener: (message: string) => void): Promise<void>
58
+ }
59
+ }
60
+
61
+ declare module 'bun:sqlite' {
62
+ export class Database {
63
+ constructor(filename: string, options?: Record<string, any>)
64
+ exec(sql: string): void
65
+ close(): void
66
+ query(sql: string): {
67
+ run(...args: any[]): any
68
+ get(...args: any[]): any
69
+ all(...args: any[]): any[]
70
+ }
71
+ transaction<T extends (...args: any[]) => any>(fn: T): T
72
+ }
73
+ }
@@ -1,11 +1,15 @@
1
1
  interface _getDoc {
2
- [Symbol.asyncIterator]<T>(): AsyncGenerator<_ttid | Record<_ttid, T>, void, unknown>;
2
+ [Symbol.asyncIterator]<T>(): AsyncGenerator<_ttid | Record<_ttid, T>, void, unknown>
3
3
  once<T>(): Promise<Record<_ttid, T>>
4
4
  onDelete(): AsyncGenerator<_ttid, void, unknown>
5
5
  }
6
6
 
7
7
  interface _findDocs {
8
- [Symbol.asyncIterator]<T>(): AsyncGenerator<_ttid | Record<_ttid, T> | Record<string, _ttid[]> | Record<_ttid, Partial<T>> | undefined, void, unknown>
8
+ [Symbol.asyncIterator]<T>(): AsyncGenerator<
9
+ _ttid | Record<_ttid, T> | Record<string, _ttid[]> | Record<_ttid, Partial<T>> | undefined,
10
+ void,
11
+ unknown
12
+ >
9
13
  once<T>(): Promise<Record<_ttid, T>>
10
14
  onDelete(): AsyncGenerator<_ttid, void, unknown>
11
15
  }
@@ -17,18 +21,29 @@ interface _queuedWriteResult {
17
21
  }
18
22
 
19
23
  interface ObjectConstructor {
20
- appendGroup: (target: Record<string, any>, source: Record<string, any>) => Record<string, any>;
24
+ appendGroup: (target: Record<string, any>, source: Record<string, any>) => Record<string, any>
21
25
  }
22
26
 
23
27
  interface Console {
24
28
  format: (docs: Record<string, any>) => void
25
29
  }
26
30
 
27
- type _joinDocs<T, U> = _ttid[] | Record<string, _ttid[]> | Record<string, Record<_ttid, Partial<T | U>>> | Record<`${_ttid}, ${_ttid}`, T | U | (T & U) | (Partial<T> & Partial<U>)>
31
+ type _joinDocs<T, U> =
32
+ | _ttid[]
33
+ | Record<string, _ttid[]>
34
+ | Record<string, Record<_ttid, Partial<T | U>>>
35
+ | Record<`${_ttid}, ${_ttid}`, T | U | (T & U) | (Partial<T> & Partial<U>)>
28
36
 
29
- declare module "@delma/fylo" {
37
+ type _fyloEngineKind = 'legacy-s3' | 's3-files'
30
38
 
39
+ interface _fyloOptions {
40
+ engine?: _fyloEngineKind
41
+ s3FilesRoot?: string
42
+ }
43
+
44
+ declare module '@delma/fylo' {
31
45
  export default class {
46
+ constructor(options?: _fyloOptions)
32
47
 
33
48
  /**
34
49
  * Rolls back all transcations in current instance
@@ -41,8 +56,10 @@ declare module "@delma/fylo" {
41
56
  * @param SQL The SQL query to execute.
42
57
  * @returns The results of the query.
43
58
  */
44
- executeSQL<T extends Record<string, any>, U extends Record<string, any> = {}>(SQL: string): Promise<number | void | any[] | _ttid | Record<any, any>>
45
-
59
+ executeSQL<T extends Record<string, any>, U extends Record<string, any> = {}>(
60
+ SQL: string
61
+ ): Promise<number | void | any[] | _ttid | Record<any, any>>
62
+
46
63
  /**
47
64
  * Creates a new schema for a collection.
48
65
  * @param collection The name of the collection.
@@ -68,7 +85,9 @@ declare module "@delma/fylo" {
68
85
  * @param collection The name of the collection.
69
86
  * @returns The current data exported from the collection.
70
87
  */
71
- exportBulkData<T extends Record<string, any>>(collection: string): AsyncGenerator<T, void, unknown>
88
+ exportBulkData<T extends Record<string, any>>(
89
+ collection: string
90
+ ): AsyncGenerator<T, void, unknown>
72
91
 
73
92
  /**
74
93
  * Gets a document from a collection.
@@ -85,11 +104,21 @@ declare module "@delma/fylo" {
85
104
  * @param batch The documents to put.
86
105
  * @returns The IDs of the documents.
87
106
  */
88
- batchPutData<T extends Record<string, any>>(collection: string, batch: Array<T>): Promise<_ttid[]>
107
+ batchPutData<T extends Record<string, any>>(
108
+ collection: string,
109
+ batch: Array<T>
110
+ ): Promise<_ttid[]>
89
111
 
90
- queuePutData<T extends Record<string, any>>(collection: string, data: Record<_ttid, T> | T): Promise<_queuedWriteResult>
112
+ queuePutData<T extends Record<string, any>>(
113
+ collection: string,
114
+ data: Record<_ttid, T> | T
115
+ ): Promise<_queuedWriteResult>
91
116
 
92
- queuePatchDoc<T extends Record<string, any>>(collection: string, newDoc: Record<_ttid, Partial<T>>, oldDoc?: Record<_ttid, T>): Promise<_queuedWriteResult>
117
+ queuePatchDoc<T extends Record<string, any>>(
118
+ collection: string,
119
+ newDoc: Record<_ttid, Partial<T>>,
120
+ oldDoc?: Record<_ttid, T>
121
+ ): Promise<_queuedWriteResult>
93
122
 
94
123
  queueDelDoc(collection: string, _id: _ttid): Promise<_queuedWriteResult>
95
124
 
@@ -99,7 +128,7 @@ declare module "@delma/fylo" {
99
128
 
100
129
  getDeadLetters(count?: number): Promise<Array<Record<string, any>>>
101
130
 
102
- getQueueStats(): Promise<{ queued: number, pending: number, deadLetters: number }>
131
+ getQueueStats(): Promise<{ queued: number; pending: number; deadLetters: number }>
103
132
 
104
133
  replayDeadLetter(streamId: string): Promise<Record<string, any> | null>
105
134
 
@@ -112,11 +141,30 @@ declare module "@delma/fylo" {
112
141
  * @returns The ID of the document.
113
142
  */
114
143
  putData<T extends Record<string, any>>(collection: string, data: T): Promise<_ttid>
115
- putData<T extends Record<string, any>>(collection: string, data: Record<_ttid, T>): Promise<_ttid>
116
- putData<T extends Record<string, any>>(collection: string, data: T, options: { wait?: true, timeoutMs?: number }): Promise<_ttid>
117
- putData<T extends Record<string, any>>(collection: string, data: Record<_ttid, T>, options: { wait?: true, timeoutMs?: number }): Promise<_ttid>
118
- putData<T extends Record<string, any>>(collection: string, data: T, options: { wait: false, timeoutMs?: number }): Promise<_queuedWriteResult>
119
- putData<T extends Record<string, any>>(collection: string, data: Record<_ttid, T>, options: { wait: false, timeoutMs?: number }): Promise<_queuedWriteResult>
144
+ putData<T extends Record<string, any>>(
145
+ collection: string,
146
+ data: Record<_ttid, T>
147
+ ): Promise<_ttid>
148
+ putData<T extends Record<string, any>>(
149
+ collection: string,
150
+ data: T,
151
+ options: { wait?: true; timeoutMs?: number }
152
+ ): Promise<_ttid>
153
+ putData<T extends Record<string, any>>(
154
+ collection: string,
155
+ data: Record<_ttid, T>,
156
+ options: { wait?: true; timeoutMs?: number }
157
+ ): Promise<_ttid>
158
+ putData<T extends Record<string, any>>(
159
+ collection: string,
160
+ data: T,
161
+ options: { wait: false; timeoutMs?: number }
162
+ ): Promise<_queuedWriteResult>
163
+ putData<T extends Record<string, any>>(
164
+ collection: string,
165
+ data: Record<_ttid, T>,
166
+ options: { wait: false; timeoutMs?: number }
167
+ ): Promise<_queuedWriteResult>
120
168
 
121
169
  /**
122
170
  * Patches a document in a collection.
@@ -125,9 +173,23 @@ declare module "@delma/fylo" {
125
173
  * @param oldDoc The old document data.
126
174
  * @returns The number of documents patched.
127
175
  */
128
- patchDoc<T extends Record<string, any>>(collection: string, newDoc: Record<_ttid, Partial<T>>, oldDoc?: Record<_ttid, T>): Promise<_ttid>
129
- patchDoc<T extends Record<string, any>>(collection: string, newDoc: Record<_ttid, Partial<T>>, oldDoc: Record<_ttid, T> | undefined, options: { wait?: true, timeoutMs?: number }): Promise<_ttid>
130
- patchDoc<T extends Record<string, any>>(collection: string, newDoc: Record<_ttid, Partial<T>>, oldDoc: Record<_ttid, T> | undefined, options: { wait: false, timeoutMs?: number }): Promise<_queuedWriteResult>
176
+ patchDoc<T extends Record<string, any>>(
177
+ collection: string,
178
+ newDoc: Record<_ttid, Partial<T>>,
179
+ oldDoc?: Record<_ttid, T>
180
+ ): Promise<_ttid>
181
+ patchDoc<T extends Record<string, any>>(
182
+ collection: string,
183
+ newDoc: Record<_ttid, Partial<T>>,
184
+ oldDoc: Record<_ttid, T> | undefined,
185
+ options: { wait?: true; timeoutMs?: number }
186
+ ): Promise<_ttid>
187
+ patchDoc<T extends Record<string, any>>(
188
+ collection: string,
189
+ newDoc: Record<_ttid, Partial<T>>,
190
+ oldDoc: Record<_ttid, T> | undefined,
191
+ options: { wait: false; timeoutMs?: number }
192
+ ): Promise<_queuedWriteResult>
131
193
 
132
194
  /**
133
195
  * Patches documents in a collection.
@@ -135,7 +197,10 @@ declare module "@delma/fylo" {
135
197
  * @param updateSchema The update schema.
136
198
  * @returns The number of documents patched.
137
199
  */
138
- patchDocs<T extends Record<string, any>>(collection: string, updateSchema: _storeUpdate<T>): Promise<number>
200
+ patchDocs<T extends Record<string, any>>(
201
+ collection: string,
202
+ updateSchema: _storeUpdate<T>
203
+ ): Promise<number>
139
204
 
140
205
  /**
141
206
  * Deletes a document from a collection.
@@ -144,8 +209,16 @@ declare module "@delma/fylo" {
144
209
  * @returns The number of documents deleted.
145
210
  */
146
211
  delDoc(collection: string, _id: _ttid): Promise<void>
147
- delDoc(collection: string, _id: _ttid, options: { wait?: true, timeoutMs?: number }): Promise<void>
148
- delDoc(collection: string, _id: _ttid, options: { wait: false, timeoutMs?: number }): Promise<_queuedWriteResult>
212
+ delDoc(
213
+ collection: string,
214
+ _id: _ttid,
215
+ options: { wait?: true; timeoutMs?: number }
216
+ ): Promise<void>
217
+ delDoc(
218
+ collection: string,
219
+ _id: _ttid,
220
+ options: { wait: false; timeoutMs?: number }
221
+ ): Promise<_queuedWriteResult>
149
222
 
150
223
  /**
151
224
  * Deletes documents from a collection.
@@ -153,21 +226,36 @@ declare module "@delma/fylo" {
153
226
  * @param deleteSchema The delete schema.
154
227
  * @returns The number of documents deleted.
155
228
  */
156
- delDocs<T extends Record<string, any>>(collection: string, deleteSchema?: _storeDelete<T>): Promise<number>
229
+ delDocs<T extends Record<string, any>>(
230
+ collection: string,
231
+ deleteSchema?: _storeDelete<T>
232
+ ): Promise<number>
157
233
 
158
234
  /**
159
235
  * Joins documents from two collections.
160
236
  * @param join The join schema.
161
237
  * @returns The joined documents.
162
238
  */
163
- static joinDocs<T extends Record<string, any>, U extends Record<string, any>>(join: _join<T, U>): Promise<_joinDocs<T, U>>
164
-
239
+ static joinDocs<T extends Record<string, any>, U extends Record<string, any>>(
240
+ join: _join<T, U>
241
+ ): Promise<_joinDocs<T, U>>
242
+
165
243
  /**
166
244
  * Finds documents in a collection.
167
245
  * @param collection The name of the collection.
168
246
  * @param query The query schema.
169
247
  * @returns The found documents.
170
248
  */
171
- static findDocs<T extends Record<string, any>>(collection: string, query?: _storeQuery<T>): _findDocs
249
+ static findDocs<T extends Record<string, any>>(
250
+ collection: string,
251
+ query?: _storeQuery<T>
252
+ ): _findDocs
172
253
  }
254
+
255
+ export function migrateLegacyS3ToS3Files(options: {
256
+ collections: string[]
257
+ s3FilesRoot?: string
258
+ recreateCollections?: boolean
259
+ verify?: boolean
260
+ }): Promise<Record<string, { migrated: number; verified: boolean }>>
173
261
  }
@@ -0,0 +1,61 @@
1
+ declare const process: {
2
+ env: Record<string, string | undefined>
3
+ }
4
+
5
+ declare const Buffer: {
6
+ alloc(size: number): Uint8Array & {
7
+ toString(encoding?: string): string
8
+ }
9
+ }
10
+
11
+ declare namespace NodeJS {
12
+ interface ErrnoException extends Error {
13
+ code?: string
14
+ }
15
+ }
16
+
17
+ declare module 'node:path' {
18
+ const path: {
19
+ join(...parts: string[]): string
20
+ dirname(target: string): string
21
+ basename(target: string, suffix?: string): string
22
+ }
23
+
24
+ export default path
25
+ }
26
+
27
+ declare module 'node:crypto' {
28
+ export function createHash(algorithm: string): {
29
+ update(value: string): {
30
+ digest(encoding: 'hex'): string
31
+ }
32
+ }
33
+ }
34
+
35
+ declare module 'node:fs/promises' {
36
+ export function mkdir(path: string, options?: { recursive?: boolean }): Promise<void>
37
+ export function readFile(path: string, encoding: string): Promise<string>
38
+ export function readdir(
39
+ path: string,
40
+ options?: { withFileTypes?: boolean }
41
+ ): Promise<
42
+ Array<{
43
+ name: string
44
+ isDirectory(): boolean
45
+ }>
46
+ >
47
+ export function rm(
48
+ path: string,
49
+ options?: { recursive?: boolean; force?: boolean }
50
+ ): Promise<void>
51
+ export function stat(path: string): Promise<{ size: number }>
52
+ export function writeFile(path: string, data: string, encoding: string): Promise<void>
53
+ export function open(
54
+ path: string,
55
+ flags: string
56
+ ): Promise<{
57
+ write(data: string): Promise<void>
58
+ read(buffer: Uint8Array, offset: number, length: number, position: number): Promise<void>
59
+ close(): Promise<void>
60
+ }>
61
+ }
@@ -33,7 +33,7 @@ type _join<T, U> = {
33
33
  $select?: Array<keyof T | keyof U>
34
34
  $leftCollection: string
35
35
  $rightCollection: string
36
- $mode: "inner" | "left" | "right" | "outer"
36
+ $mode: 'inner' | 'left' | 'right' | 'outer'
37
37
  $on: _on<T, U>
38
38
  $limit?: number
39
39
  $onlyIds?: boolean
@@ -53,7 +53,11 @@ interface _storeQuery<T extends Record<string, any>> {
53
53
  $created?: _timestamp
54
54
  }
55
55
 
56
- interface _condition { column: string, operator: string, value: string | number| boolean | null }
56
+ interface _condition {
57
+ column: string
58
+ operator: string
59
+ value: string | number | boolean | null
60
+ }
57
61
 
58
62
  type _storeUpdate<T extends Record<string, any>> = {
59
63
  $collection?: string
@@ -1,5 +1,4 @@
1
- declare module "@delma/ttid" {
2
-
1
+ declare module '@delma/ttid' {
3
2
  export type _ttid = string | `${string}-${string}` | `${string}-${string}-${string}`
4
3
 
5
4
  export interface _timestamps {
@@ -16,16 +15,18 @@ declare module "@delma/ttid" {
16
15
  }
17
16
  }
18
17
 
19
- declare module "@delma/chex" {
20
-
18
+ declare module '@delma/chex' {
21
19
  export default class Gen {
22
20
  static generateDeclaration(json: unknown, interfaceName?: string): string
23
21
  static sanitizePropertyName(key: string): string
24
22
  static fromJsonString(jsonString: string, interfaceName?: string): string
25
23
  static fromObject(obj: unknown, interfaceName?: string): string
26
- static validateData<T extends Record<string, unknown>>(collection: string, data: T): Promise<T>
24
+ static validateData<T extends Record<string, unknown>>(
25
+ collection: string,
26
+ data: T
27
+ ): Promise<T>
27
28
  }
28
29
  }
29
30
 
30
- type _ttid = import("@delma/ttid")._ttid
31
- type _timestamps = import("@delma/ttid")._timestamps
31
+ type _ttid = import('@delma/ttid')._ttid
32
+ type _timestamps = import('@delma/ttid')._timestamps
package/src/worker.ts CHANGED
@@ -1,12 +1,18 @@
1
1
  #!/usr/bin/env bun
2
2
  import { WriteWorker } from './workers/write-worker'
3
3
 
4
+ if (process.env.FYLO_STORAGE_ENGINE === 's3-files') {
5
+ throw new Error('fylo.worker is not supported in s3-files engine')
6
+ }
7
+
4
8
  const worker = new WriteWorker(process.env.FYLO_WORKER_ID)
5
9
 
6
10
  await worker.run({
7
11
  batchSize: process.env.FYLO_WORKER_BATCH_SIZE ? Number(process.env.FYLO_WORKER_BATCH_SIZE) : 1,
8
12
  blockMs: process.env.FYLO_WORKER_BLOCK_MS ? Number(process.env.FYLO_WORKER_BLOCK_MS) : 1000,
9
13
  recoverOnStart: process.env.FYLO_WORKER_RECOVER_ON_START !== 'false',
10
- recoverIdleMs: process.env.FYLO_WORKER_RECOVER_IDLE_MS ? Number(process.env.FYLO_WORKER_RECOVER_IDLE_MS) : 30_000,
14
+ recoverIdleMs: process.env.FYLO_WORKER_RECOVER_IDLE_MS
15
+ ? Number(process.env.FYLO_WORKER_RECOVER_IDLE_MS)
16
+ : 30_000,
11
17
  stopWhenIdle: process.env.FYLO_WORKER_STOP_WHEN_IDLE === 'true'
12
18
  })
@@ -1,9 +1,8 @@
1
- import Fylo from "../index"
2
- import { Redis } from "../adapters/redis"
3
- import type { StreamJobEntry, WriteJob } from "../types/write-queue"
1
+ import Fylo from '../index'
2
+ import { Redis } from '../adapters/redis'
3
+ import type { StreamJobEntry, WriteJob } from '../types/write-queue'
4
4
 
5
5
  export class WriteWorker {
6
-
7
6
  private static readonly MAX_WRITE_ATTEMPTS = Number(process.env.FYLO_WRITE_MAX_ATTEMPTS ?? 3)
8
7
 
9
8
  private static readonly WRITE_RETRY_BASE_MS = Number(process.env.FYLO_WRITE_RETRY_BASE_MS ?? 10)
@@ -22,21 +21,21 @@ export class WriteWorker {
22
21
 
23
22
  async recoverPending(minIdleMs: number = 30_000, count: number = 10) {
24
23
  const jobs = await this.redis.claimPendingJobs(this.workerId, minIdleMs, count)
25
- for(const job of jobs) await this.processJob(job)
24
+ for (const job of jobs) await this.processJob(job)
26
25
  return jobs.length
27
26
  }
28
27
 
29
28
  async processNext(count: number = 1, blockMs: number = 1000) {
30
29
  const jobs = await this.redis.readWriteJobs(this.workerId, count, blockMs)
31
- for(const job of jobs) await this.processJob(job)
30
+ for (const job of jobs) await this.processJob(job)
32
31
  return jobs.length
33
32
  }
34
33
 
35
34
  async processJob({ streamId, job }: StreamJobEntry) {
36
- if(job.nextAttemptAt && job.nextAttemptAt > Date.now()) return false
35
+ if (job.nextAttemptAt && job.nextAttemptAt > Date.now()) return false
37
36
 
38
37
  const locked = await this.redis.acquireDocLock(job.collection, job.docId, job.jobId)
39
- if(!locked) return false
38
+ if (!locked) return false
40
39
 
41
40
  try {
42
41
  await this.redis.setJobStatus(job.jobId, 'processing', {
@@ -52,29 +51,33 @@ export class WriteWorker {
52
51
  await this.redis.ackWriteJob(streamId)
53
52
 
54
53
  return true
55
-
56
- } catch(err) {
54
+ } catch (err) {
57
55
  const attempts = job.attempts + 1
58
56
  const message = err instanceof Error ? err.message : String(err)
59
57
 
60
- if(attempts >= WriteWorker.MAX_WRITE_ATTEMPTS) {
58
+ if (attempts >= WriteWorker.MAX_WRITE_ATTEMPTS) {
61
59
  await this.redis.setJobStatus(job.jobId, 'dead-letter', {
62
60
  workerId: this.workerId,
63
61
  attempts,
64
62
  error: message
65
63
  })
66
64
  await this.redis.setDocStatus(job.collection, job.docId, 'dead-letter', job.jobId)
67
- await this.redis.deadLetterWriteJob(streamId, {
68
- ...job,
69
- attempts,
70
- status: 'dead-letter',
71
- workerId: this.workerId,
72
- error: message
73
- }, message)
65
+ await this.redis.deadLetterWriteJob(
66
+ streamId,
67
+ {
68
+ ...job,
69
+ attempts,
70
+ status: 'dead-letter',
71
+ workerId: this.workerId,
72
+ error: message
73
+ },
74
+ message
75
+ )
74
76
  return false
75
77
  }
76
78
 
77
- const nextAttemptAt = Date.now() + (WriteWorker.WRITE_RETRY_BASE_MS * Math.max(1, 2 ** (attempts - 1)))
79
+ const nextAttemptAt =
80
+ Date.now() + WriteWorker.WRITE_RETRY_BASE_MS * Math.max(1, 2 ** (attempts - 1))
78
81
 
79
82
  await this.redis.setJobStatus(job.jobId, 'failed', {
80
83
  workerId: this.workerId,
@@ -85,7 +88,6 @@ export class WriteWorker {
85
88
  await this.redis.setDocStatus(job.collection, job.docId, 'failed', job.jobId)
86
89
 
87
90
  return false
88
-
89
91
  } finally {
90
92
  await this.redis.releaseDocLock(job.collection, job.docId, job.jobId)
91
93
  }
@@ -108,12 +110,11 @@ export class WriteWorker {
108
110
  recoverIdleMs?: number
109
111
  stopWhenIdle?: boolean
110
112
  } = {}) {
113
+ if (recoverOnStart) await this.recoverPending(recoverIdleMs, batchSize)
111
114
 
112
- if(recoverOnStart) await this.recoverPending(recoverIdleMs, batchSize)
113
-
114
- while(true) {
115
+ while (true) {
115
116
  const processed = await this.processNext(batchSize, blockMs)
116
- if(stopWhenIdle && processed === 0) break
117
+ if (stopWhenIdle && processed === 0) break
117
118
  }
118
119
  }
119
120
  }