@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/migrate-cli.ts
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,74 +0,0 @@
|
|
|
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
|
-
}
|
package/src/types/fylo.d.ts
DELETED
|
@@ -1,261 +0,0 @@
|
|
|
1
|
-
interface _getDoc {
|
|
2
|
-
[Symbol.asyncIterator]<T>(): AsyncGenerator<_ttid | Record<_ttid, T>, void, unknown>
|
|
3
|
-
once<T>(): Promise<Record<_ttid, T>>
|
|
4
|
-
onDelete(): AsyncGenerator<_ttid, void, unknown>
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
interface _findDocs {
|
|
8
|
-
[Symbol.asyncIterator]<T>(): AsyncGenerator<
|
|
9
|
-
_ttid | Record<_ttid, T> | Record<string, _ttid[]> | Record<_ttid, Partial<T>> | undefined,
|
|
10
|
-
void,
|
|
11
|
-
unknown
|
|
12
|
-
>
|
|
13
|
-
once<T>(): Promise<Record<_ttid, T>>
|
|
14
|
-
onDelete(): AsyncGenerator<_ttid, void, unknown>
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
interface _queuedWriteResult {
|
|
18
|
-
jobId: string
|
|
19
|
-
docId: _ttid
|
|
20
|
-
status: 'queued' | 'processing' | 'committed' | 'failed' | 'dead-letter'
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
interface ObjectConstructor {
|
|
24
|
-
appendGroup: (target: Record<string, any>, source: Record<string, any>) => Record<string, any>
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
interface Console {
|
|
28
|
-
format: (docs: Record<string, any>) => void
|
|
29
|
-
}
|
|
30
|
-
|
|
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>)>
|
|
36
|
-
|
|
37
|
-
type _fyloEngineKind = 'legacy-s3' | 's3-files'
|
|
38
|
-
|
|
39
|
-
interface _fyloOptions {
|
|
40
|
-
engine?: _fyloEngineKind
|
|
41
|
-
s3FilesRoot?: string
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
declare module '@delma/fylo' {
|
|
45
|
-
export default class {
|
|
46
|
-
constructor(options?: _fyloOptions)
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Rolls back all transcations in current instance
|
|
50
|
-
* @deprecated Prefer queued write recovery, dead letters, or compensating writes.
|
|
51
|
-
*/
|
|
52
|
-
rollback(): Promise<void>
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Executes a SQL query and returns the results.
|
|
56
|
-
* @param SQL The SQL query to execute.
|
|
57
|
-
* @returns The results of the query.
|
|
58
|
-
*/
|
|
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
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Creates a new schema for a collection.
|
|
65
|
-
* @param collection The name of the collection.
|
|
66
|
-
*/
|
|
67
|
-
static createCollection(collection: string): Promise<void>
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Drops an existing schema for a collection.
|
|
71
|
-
* @param collection The name of the collection.
|
|
72
|
-
*/
|
|
73
|
-
static dropCollection(collection: string): Promise<void>
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Imports data from a URL into a collection.
|
|
77
|
-
* @param collection The name of the collection.
|
|
78
|
-
* @param url The URL of the data to import.
|
|
79
|
-
* @param limit The maximum number of documents to import.
|
|
80
|
-
*/
|
|
81
|
-
importBulkData(collection: string, url: URL, limit?: number): Promise<number>
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Exports data from a collection to a URL.
|
|
85
|
-
* @param collection The name of the collection.
|
|
86
|
-
* @returns The current data exported from the collection.
|
|
87
|
-
*/
|
|
88
|
-
exportBulkData<T extends Record<string, any>>(
|
|
89
|
-
collection: string
|
|
90
|
-
): AsyncGenerator<T, void, unknown>
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Gets a document from a collection.
|
|
94
|
-
* @param collection The name of the collection.
|
|
95
|
-
* @param _id The ID of the document.
|
|
96
|
-
* @param onlyId Whether to only return the ID of the document.
|
|
97
|
-
* @returns The document or the ID of the document.
|
|
98
|
-
*/
|
|
99
|
-
static getDoc(collection: string, _id: _ttid, onlyId: boolean): _getDoc
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Puts multiple documents into a collection.
|
|
103
|
-
* @param collection The name of the collection.
|
|
104
|
-
* @param batch The documents to put.
|
|
105
|
-
* @returns The IDs of the documents.
|
|
106
|
-
*/
|
|
107
|
-
batchPutData<T extends Record<string, any>>(
|
|
108
|
-
collection: string,
|
|
109
|
-
batch: Array<T>
|
|
110
|
-
): Promise<_ttid[]>
|
|
111
|
-
|
|
112
|
-
queuePutData<T extends Record<string, any>>(
|
|
113
|
-
collection: string,
|
|
114
|
-
data: Record<_ttid, T> | T
|
|
115
|
-
): Promise<_queuedWriteResult>
|
|
116
|
-
|
|
117
|
-
queuePatchDoc<T extends Record<string, any>>(
|
|
118
|
-
collection: string,
|
|
119
|
-
newDoc: Record<_ttid, Partial<T>>,
|
|
120
|
-
oldDoc?: Record<_ttid, T>
|
|
121
|
-
): Promise<_queuedWriteResult>
|
|
122
|
-
|
|
123
|
-
queueDelDoc(collection: string, _id: _ttid): Promise<_queuedWriteResult>
|
|
124
|
-
|
|
125
|
-
getJobStatus(jobId: string): Promise<Record<string, any> | null>
|
|
126
|
-
|
|
127
|
-
getDocStatus(collection: string, docId: _ttid): Promise<Record<string, any> | null>
|
|
128
|
-
|
|
129
|
-
getDeadLetters(count?: number): Promise<Array<Record<string, any>>>
|
|
130
|
-
|
|
131
|
-
getQueueStats(): Promise<{ queued: number; pending: number; deadLetters: number }>
|
|
132
|
-
|
|
133
|
-
replayDeadLetter(streamId: string): Promise<Record<string, any> | null>
|
|
134
|
-
|
|
135
|
-
processQueuedWrites(count?: number, recover?: boolean): Promise<number>
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Puts a document into a collection.
|
|
139
|
-
* @param collection The name of the collection.
|
|
140
|
-
* @param data The document to put.
|
|
141
|
-
* @returns The ID of the document.
|
|
142
|
-
*/
|
|
143
|
-
putData<T extends Record<string, any>>(collection: string, data: T): Promise<_ttid>
|
|
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>
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* Patches a document in a collection.
|
|
171
|
-
* @param collection The name of the collection.
|
|
172
|
-
* @param newDoc The new document data.
|
|
173
|
-
* @param oldDoc The old document data.
|
|
174
|
-
* @returns The number of documents patched.
|
|
175
|
-
*/
|
|
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>
|
|
193
|
-
|
|
194
|
-
/**
|
|
195
|
-
* Patches documents in a collection.
|
|
196
|
-
* @param collection The name of the collection.
|
|
197
|
-
* @param updateSchema The update schema.
|
|
198
|
-
* @returns The number of documents patched.
|
|
199
|
-
*/
|
|
200
|
-
patchDocs<T extends Record<string, any>>(
|
|
201
|
-
collection: string,
|
|
202
|
-
updateSchema: _storeUpdate<T>
|
|
203
|
-
): Promise<number>
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* Deletes a document from a collection.
|
|
207
|
-
* @param collection The name of the collection.
|
|
208
|
-
* @param _id The ID of the document.
|
|
209
|
-
* @returns The number of documents deleted.
|
|
210
|
-
*/
|
|
211
|
-
delDoc(collection: string, _id: _ttid): Promise<void>
|
|
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>
|
|
222
|
-
|
|
223
|
-
/**
|
|
224
|
-
* Deletes documents from a collection.
|
|
225
|
-
* @param collection The name of the collection.
|
|
226
|
-
* @param deleteSchema The delete schema.
|
|
227
|
-
* @returns The number of documents deleted.
|
|
228
|
-
*/
|
|
229
|
-
delDocs<T extends Record<string, any>>(
|
|
230
|
-
collection: string,
|
|
231
|
-
deleteSchema?: _storeDelete<T>
|
|
232
|
-
): Promise<number>
|
|
233
|
-
|
|
234
|
-
/**
|
|
235
|
-
* Joins documents from two collections.
|
|
236
|
-
* @param join The join schema.
|
|
237
|
-
* @returns The joined documents.
|
|
238
|
-
*/
|
|
239
|
-
static joinDocs<T extends Record<string, any>, U extends Record<string, any>>(
|
|
240
|
-
join: _join<T, U>
|
|
241
|
-
): Promise<_joinDocs<T, U>>
|
|
242
|
-
|
|
243
|
-
/**
|
|
244
|
-
* Finds documents in a collection.
|
|
245
|
-
* @param collection The name of the collection.
|
|
246
|
-
* @param query The query schema.
|
|
247
|
-
* @returns The found documents.
|
|
248
|
-
*/
|
|
249
|
-
static findDocs<T extends Record<string, any>>(
|
|
250
|
-
collection: string,
|
|
251
|
-
query?: _storeQuery<T>
|
|
252
|
-
): _findDocs
|
|
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 }>>
|
|
261
|
-
}
|
package/src/types/write-queue.ts
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
export type WriteJobOperation = 'insert' | 'update' | 'delete'
|
|
2
|
-
|
|
3
|
-
export type WriteJobStatus = 'queued' | 'processing' | 'committed' | 'failed' | 'dead-letter'
|
|
4
|
-
|
|
5
|
-
export interface WriteJob<T extends Record<string, any> = Record<string, any>> {
|
|
6
|
-
jobId: string
|
|
7
|
-
collection: string
|
|
8
|
-
docId: _ttid
|
|
9
|
-
operation: WriteJobOperation
|
|
10
|
-
payload: T
|
|
11
|
-
status: WriteJobStatus
|
|
12
|
-
attempts: number
|
|
13
|
-
createdAt: number
|
|
14
|
-
updatedAt: number
|
|
15
|
-
nextAttemptAt?: number
|
|
16
|
-
workerId?: string
|
|
17
|
-
error?: string
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export interface QueuedWriteResult {
|
|
21
|
-
jobId: string
|
|
22
|
-
docId: _ttid
|
|
23
|
-
status: WriteJobStatus
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export interface StreamJobEntry<T extends Record<string, any> = Record<string, any>> {
|
|
27
|
-
streamId: string
|
|
28
|
-
job: WriteJob<T>
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export interface DeadLetterJob<T extends Record<string, any> = Record<string, any>> {
|
|
32
|
-
streamId: string
|
|
33
|
-
job: WriteJob<T>
|
|
34
|
-
reason?: string
|
|
35
|
-
failedAt: number
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export interface QueueStats {
|
|
39
|
-
queued: number
|
|
40
|
-
pending: number
|
|
41
|
-
deadLetters: number
|
|
42
|
-
}
|
package/src/worker.ts
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
import { WriteWorker } from './workers/write-worker'
|
|
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
|
-
|
|
8
|
-
const worker = new WriteWorker(process.env.FYLO_WORKER_ID)
|
|
9
|
-
|
|
10
|
-
await worker.run({
|
|
11
|
-
batchSize: process.env.FYLO_WORKER_BATCH_SIZE ? Number(process.env.FYLO_WORKER_BATCH_SIZE) : 1,
|
|
12
|
-
blockMs: process.env.FYLO_WORKER_BLOCK_MS ? Number(process.env.FYLO_WORKER_BLOCK_MS) : 1000,
|
|
13
|
-
recoverOnStart: process.env.FYLO_WORKER_RECOVER_ON_START !== 'false',
|
|
14
|
-
recoverIdleMs: process.env.FYLO_WORKER_RECOVER_IDLE_MS
|
|
15
|
-
? Number(process.env.FYLO_WORKER_RECOVER_IDLE_MS)
|
|
16
|
-
: 30_000,
|
|
17
|
-
stopWhenIdle: process.env.FYLO_WORKER_STOP_WHEN_IDLE === 'true'
|
|
18
|
-
})
|
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
import Fylo from '../index'
|
|
2
|
-
import { Redis } from '../adapters/redis'
|
|
3
|
-
import type { StreamJobEntry, WriteJob } from '../types/write-queue'
|
|
4
|
-
|
|
5
|
-
export class WriteWorker {
|
|
6
|
-
private static readonly MAX_WRITE_ATTEMPTS = Number(process.env.FYLO_WRITE_MAX_ATTEMPTS ?? 3)
|
|
7
|
-
|
|
8
|
-
private static readonly WRITE_RETRY_BASE_MS = Number(process.env.FYLO_WRITE_RETRY_BASE_MS ?? 10)
|
|
9
|
-
|
|
10
|
-
private readonly fylo: Fylo
|
|
11
|
-
|
|
12
|
-
private readonly redis: Redis
|
|
13
|
-
|
|
14
|
-
readonly workerId: string
|
|
15
|
-
|
|
16
|
-
constructor(workerId: string = Bun.randomUUIDv7()) {
|
|
17
|
-
this.workerId = workerId
|
|
18
|
-
this.fylo = new Fylo()
|
|
19
|
-
this.redis = new Redis()
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
async recoverPending(minIdleMs: number = 30_000, count: number = 10) {
|
|
23
|
-
const jobs = await this.redis.claimPendingJobs(this.workerId, minIdleMs, count)
|
|
24
|
-
for (const job of jobs) await this.processJob(job)
|
|
25
|
-
return jobs.length
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
async processNext(count: number = 1, blockMs: number = 1000) {
|
|
29
|
-
const jobs = await this.redis.readWriteJobs(this.workerId, count, blockMs)
|
|
30
|
-
for (const job of jobs) await this.processJob(job)
|
|
31
|
-
return jobs.length
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
async processJob({ streamId, job }: StreamJobEntry) {
|
|
35
|
-
if (job.nextAttemptAt && job.nextAttemptAt > Date.now()) return false
|
|
36
|
-
|
|
37
|
-
const locked = await this.redis.acquireDocLock(job.collection, job.docId, job.jobId)
|
|
38
|
-
if (!locked) return false
|
|
39
|
-
|
|
40
|
-
try {
|
|
41
|
-
await this.redis.setJobStatus(job.jobId, 'processing', {
|
|
42
|
-
workerId: this.workerId,
|
|
43
|
-
attempts: job.attempts + 1
|
|
44
|
-
})
|
|
45
|
-
await this.redis.setDocStatus(job.collection, job.docId, 'processing', job.jobId)
|
|
46
|
-
|
|
47
|
-
await this.fylo.executeQueuedWrite(job)
|
|
48
|
-
|
|
49
|
-
await this.redis.setJobStatus(job.jobId, 'committed', { workerId: this.workerId })
|
|
50
|
-
await this.redis.setDocStatus(job.collection, job.docId, 'committed', job.jobId)
|
|
51
|
-
await this.redis.ackWriteJob(streamId)
|
|
52
|
-
|
|
53
|
-
return true
|
|
54
|
-
} catch (err) {
|
|
55
|
-
const attempts = job.attempts + 1
|
|
56
|
-
const message = err instanceof Error ? err.message : String(err)
|
|
57
|
-
|
|
58
|
-
if (attempts >= WriteWorker.MAX_WRITE_ATTEMPTS) {
|
|
59
|
-
await this.redis.setJobStatus(job.jobId, 'dead-letter', {
|
|
60
|
-
workerId: this.workerId,
|
|
61
|
-
attempts,
|
|
62
|
-
error: message
|
|
63
|
-
})
|
|
64
|
-
await this.redis.setDocStatus(job.collection, job.docId, 'dead-letter', job.jobId)
|
|
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
|
-
)
|
|
76
|
-
return false
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const nextAttemptAt =
|
|
80
|
-
Date.now() + WriteWorker.WRITE_RETRY_BASE_MS * Math.max(1, 2 ** (attempts - 1))
|
|
81
|
-
|
|
82
|
-
await this.redis.setJobStatus(job.jobId, 'failed', {
|
|
83
|
-
workerId: this.workerId,
|
|
84
|
-
attempts,
|
|
85
|
-
error: message,
|
|
86
|
-
nextAttemptAt
|
|
87
|
-
})
|
|
88
|
-
await this.redis.setDocStatus(job.collection, job.docId, 'failed', job.jobId)
|
|
89
|
-
|
|
90
|
-
return false
|
|
91
|
-
} finally {
|
|
92
|
-
await this.redis.releaseDocLock(job.collection, job.docId, job.jobId)
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
async processQueuedInsert(job: WriteJob) {
|
|
97
|
-
return await this.fylo.executeQueuedWrite(job)
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
async run({
|
|
101
|
-
batchSize = 1,
|
|
102
|
-
blockMs = 1000,
|
|
103
|
-
recoverOnStart = true,
|
|
104
|
-
recoverIdleMs = 30_000,
|
|
105
|
-
stopWhenIdle = false
|
|
106
|
-
}: {
|
|
107
|
-
batchSize?: number
|
|
108
|
-
blockMs?: number
|
|
109
|
-
recoverOnStart?: boolean
|
|
110
|
-
recoverIdleMs?: number
|
|
111
|
-
stopWhenIdle?: boolean
|
|
112
|
-
} = {}) {
|
|
113
|
-
if (recoverOnStart) await this.recoverPending(recoverIdleMs, batchSize)
|
|
114
|
-
|
|
115
|
-
while (true) {
|
|
116
|
-
const processed = await this.processNext(batchSize, blockMs)
|
|
117
|
-
if (stopWhenIdle && processed === 0) break
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
}
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import { test, expect, describe, afterAll, mock } from 'bun:test'
|
|
2
|
-
import Fylo from '../../src'
|
|
3
|
-
import { postsURL, albumURL } from '../data'
|
|
4
|
-
import S3Mock from '../mocks/s3'
|
|
5
|
-
import RedisMock from '../mocks/redis'
|
|
6
|
-
const POSTS = `post`
|
|
7
|
-
const ALBUMS = `album`
|
|
8
|
-
afterAll(async () => {
|
|
9
|
-
await Promise.all([Fylo.dropCollection(ALBUMS), Fylo.dropCollection(POSTS)])
|
|
10
|
-
})
|
|
11
|
-
mock.module('../../src/adapters/s3', () => ({ S3: S3Mock }))
|
|
12
|
-
mock.module('../../src/adapters/redis', () => ({ Redis: RedisMock }))
|
|
13
|
-
describe('NO-SQL', () => {
|
|
14
|
-
test('TRUNCATE', async () => {
|
|
15
|
-
const fylo = new Fylo()
|
|
16
|
-
await Fylo.createCollection(POSTS)
|
|
17
|
-
await fylo.importBulkData(POSTS, new URL(postsURL))
|
|
18
|
-
await fylo.delDocs(POSTS)
|
|
19
|
-
const ids = []
|
|
20
|
-
for await (const data of Fylo.findDocs(POSTS, { $limit: 1, $onlyIds: true }).collect()) {
|
|
21
|
-
ids.push(data)
|
|
22
|
-
}
|
|
23
|
-
expect(ids.length).toBe(0)
|
|
24
|
-
})
|
|
25
|
-
})
|
|
26
|
-
describe('SQL', () => {
|
|
27
|
-
test('TRUNCATE', async () => {
|
|
28
|
-
const fylo = new Fylo()
|
|
29
|
-
await fylo.executeSQL(`CREATE TABLE ${ALBUMS}`)
|
|
30
|
-
await fylo.importBulkData(ALBUMS, new URL(albumURL))
|
|
31
|
-
await fylo.executeSQL(`DELETE FROM ${ALBUMS}`)
|
|
32
|
-
const ids = await fylo.executeSQL(`SELECT _id FROM ${ALBUMS} LIMIT 1`)
|
|
33
|
-
expect(ids.length).toBe(0)
|
|
34
|
-
})
|
|
35
|
-
})
|
package/tests/data.js
DELETED
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
function makeDataUrl(data) {
|
|
2
|
-
return `data:application/json,${encodeURIComponent(JSON.stringify(data))}`
|
|
3
|
-
}
|
|
4
|
-
function generateAlbums() {
|
|
5
|
-
return Array.from({ length: 100 }, (_, index) => {
|
|
6
|
-
const id = index + 1
|
|
7
|
-
const userId = Math.ceil(id / 10)
|
|
8
|
-
const prefix = id <= 15 ? 'omnis' : id % 4 === 0 ? 'quidem' : 'album'
|
|
9
|
-
return {
|
|
10
|
-
id,
|
|
11
|
-
userId,
|
|
12
|
-
title: `${prefix} album ${id}`
|
|
13
|
-
}
|
|
14
|
-
})
|
|
15
|
-
}
|
|
16
|
-
function generatePosts() {
|
|
17
|
-
return Array.from({ length: 100 }, (_, index) => {
|
|
18
|
-
const id = index + 1
|
|
19
|
-
const userId = Math.ceil(id / 10)
|
|
20
|
-
return {
|
|
21
|
-
id,
|
|
22
|
-
userId,
|
|
23
|
-
title: `post title ${id}`,
|
|
24
|
-
body: `post body ${id} for user ${userId}`
|
|
25
|
-
}
|
|
26
|
-
})
|
|
27
|
-
}
|
|
28
|
-
function generateComments() {
|
|
29
|
-
return Array.from({ length: 100 }, (_, index) => {
|
|
30
|
-
const id = index + 1
|
|
31
|
-
return {
|
|
32
|
-
id,
|
|
33
|
-
postId: id,
|
|
34
|
-
name: `comment ${id}`,
|
|
35
|
-
email: `comment${id}@example.com`,
|
|
36
|
-
body: `comment body ${id}`
|
|
37
|
-
}
|
|
38
|
-
})
|
|
39
|
-
}
|
|
40
|
-
function generatePhotos() {
|
|
41
|
-
return Array.from({ length: 100 }, (_, index) => {
|
|
42
|
-
const id = index + 1
|
|
43
|
-
const title = id % 3 === 0 ? `test photo ${id}` : `photo ${id}`
|
|
44
|
-
return {
|
|
45
|
-
id,
|
|
46
|
-
albumId: Math.ceil(id / 10),
|
|
47
|
-
title,
|
|
48
|
-
url: `https://example.com/photos/${id}.jpg`,
|
|
49
|
-
thumbnailUrl: `https://example.com/photos/${id}-thumb.jpg`
|
|
50
|
-
}
|
|
51
|
-
})
|
|
52
|
-
}
|
|
53
|
-
function generateTodos() {
|
|
54
|
-
return Array.from({ length: 100 }, (_, index) => {
|
|
55
|
-
const id = index + 1
|
|
56
|
-
return {
|
|
57
|
-
id,
|
|
58
|
-
userId: Math.ceil(id / 10),
|
|
59
|
-
title: id % 4 === 0 ? `test todo ${id}` : `todo ${id}`,
|
|
60
|
-
completed: id % 2 === 0
|
|
61
|
-
}
|
|
62
|
-
})
|
|
63
|
-
}
|
|
64
|
-
function generateUsers() {
|
|
65
|
-
return Array.from({ length: 10 }, (_, index) => {
|
|
66
|
-
const id = index + 1
|
|
67
|
-
return {
|
|
68
|
-
id,
|
|
69
|
-
name: `User ${id}`,
|
|
70
|
-
username: `user${id}`,
|
|
71
|
-
email: `user${id}@example.com`,
|
|
72
|
-
address: {
|
|
73
|
-
street: `Main Street ${id}`,
|
|
74
|
-
suite: `Suite ${id}`,
|
|
75
|
-
city: id <= 5 ? 'South Christy' : 'North Christy',
|
|
76
|
-
zipcode: `0000${id}`,
|
|
77
|
-
geo: {
|
|
78
|
-
lat: 10 + id,
|
|
79
|
-
lng: -20 - id
|
|
80
|
-
}
|
|
81
|
-
},
|
|
82
|
-
phone: `555-000-${String(id).padStart(4, '0')}`,
|
|
83
|
-
website: `user${id}.example.com`,
|
|
84
|
-
company: {
|
|
85
|
-
name: id <= 5 ? 'Acme Labs' : 'Northwind Labs',
|
|
86
|
-
catchPhrase: `Catch phrase ${id}`,
|
|
87
|
-
bs: `business ${id}`
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
})
|
|
91
|
-
}
|
|
92
|
-
export const albumURL = makeDataUrl(generateAlbums())
|
|
93
|
-
export const postsURL = makeDataUrl(generatePosts())
|
|
94
|
-
export const commentsURL = makeDataUrl(generateComments())
|
|
95
|
-
export const photosURL = makeDataUrl(generatePhotos())
|
|
96
|
-
export const todosURL = makeDataUrl(generateTodos())
|
|
97
|
-
export const usersURL = makeDataUrl(generateUsers())
|
package/tests/index.js
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import { Redis } from '../src/adapters/redis'
|
|
2
|
-
import ttid from '@delma/ttid'
|
|
3
|
-
const redisPub = new Redis()
|
|
4
|
-
const redisSub = new Redis()
|
|
5
|
-
setTimeout(async () => {
|
|
6
|
-
await redisPub.publish('bun', 'insert', ttid.generate())
|
|
7
|
-
}, 2000)
|
|
8
|
-
setTimeout(async () => {
|
|
9
|
-
await redisPub.publish('bun', 'insert', ttid.generate())
|
|
10
|
-
}, 3000)
|
|
11
|
-
await Bun.sleep(1000)
|
|
12
|
-
for await (const data of redisSub.subscribe('bun')) {
|
|
13
|
-
console.log('Received:', data)
|
|
14
|
-
}
|