on-zero 0.7.2-1784530701923 → 0.7.2
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/dist/cjs/asyncTasks.cjs +51 -0
- package/dist/cjs/asyncTasks.native.js +84 -0
- package/dist/cjs/asyncTasks.native.js.map +1 -0
- package/dist/cjs/createZeroDirectServer.cjs +269 -0
- package/dist/cjs/createZeroDirectServer.native.js +287 -0
- package/dist/cjs/createZeroDirectServer.native.js.map +1 -0
- package/dist/cjs/createZeroSQLiteServer.cjs +50 -0
- package/dist/cjs/createZeroSQLiteServer.native.js +58 -0
- package/dist/cjs/createZeroSQLiteServer.native.js.map +1 -0
- package/dist/cjs/createZeroSQLiteServer.test.cjs +595 -0
- package/dist/cjs/createZeroSQLiteServer.test.native.js +647 -0
- package/dist/cjs/createZeroSQLiteServer.test.native.js.map +1 -0
- package/dist/cjs/createZeroServer.cjs +82 -0
- package/dist/cjs/createZeroServer.native.js +93 -0
- package/dist/cjs/createZeroServer.native.js.map +1 -0
- package/dist/cjs/createZeroServer.test.cjs +378 -0
- package/dist/cjs/createZeroServer.test.native.js +448 -0
- package/dist/cjs/createZeroServer.test.native.js.map +1 -0
- package/dist/cjs/createZeroServerCore.cjs +211 -0
- package/dist/cjs/createZeroServerCore.native.js +355 -0
- package/dist/cjs/createZeroServerCore.native.js.map +1 -0
- package/dist/cjs/helpers/createMutators.cjs +3 -4
- package/dist/cjs/helpers/createMutators.native.js +3 -6
- package/dist/cjs/helpers/createMutators.native.js.map +1 -1
- package/dist/cjs/httpPull/auth.test.cjs +197 -0
- package/dist/cjs/httpPull/auth.test.native.js +279 -0
- package/dist/cjs/httpPull/auth.test.native.js.map +1 -0
- package/dist/cjs/httpPull/churn.test.cjs +132 -0
- package/dist/cjs/httpPull/churn.test.native.js +155 -0
- package/dist/cjs/httpPull/churn.test.native.js.map +1 -0
- package/dist/cjs/httpPull/completeAck.test.cjs +69 -0
- package/dist/cjs/httpPull/completeAck.test.native.js +76 -0
- package/dist/cjs/httpPull/completeAck.test.native.js.map +1 -0
- package/dist/cjs/httpPull/fixtureServer.cjs +340 -0
- package/dist/cjs/httpPull/fixtureServer.native.js +534 -0
- package/dist/cjs/httpPull/fixtureServer.native.js.map +1 -0
- package/dist/cjs/httpPull/integration.test.cjs +53 -0
- package/dist/cjs/httpPull/integration.test.native.js +60 -0
- package/dist/cjs/httpPull/integration.test.native.js.map +1 -0
- package/dist/cjs/httpPull/rebase.test.cjs +360 -0
- package/dist/cjs/httpPull/rebase.test.native.js +420 -0
- package/dist/cjs/httpPull/rebase.test.native.js.map +1 -0
- package/dist/cjs/httpPull/relations.test.cjs +107 -0
- package/dist/cjs/httpPull/relations.test.native.js +119 -0
- package/dist/cjs/httpPull/relations.test.native.js.map +1 -0
- package/dist/cjs/httpPull/testHarness.cjs +100 -0
- package/dist/cjs/httpPull/testHarness.native.js +112 -0
- package/dist/cjs/httpPull/testHarness.native.js.map +1 -0
- package/dist/cjs/index.cjs +1 -0
- package/dist/cjs/index.native.js +1 -0
- package/dist/cjs/index.native.js.map +1 -1
- package/dist/cjs/run.cjs +1 -1
- package/dist/cjs/run.native.js +1 -1
- package/dist/cjs/server.cjs +22 -0
- package/dist/cjs/server.native.js +4 -0
- package/dist/cjs/server.native.js.map +1 -0
- package/dist/cjs/sqliteRuntime.cjs +318 -0
- package/dist/cjs/sqliteRuntime.native.js +438 -0
- package/dist/cjs/sqliteRuntime.native.js.map +1 -0
- package/dist/cjs/state.cjs +1 -1
- package/dist/cjs/state.native.js +1 -1
- package/dist/cjs/syncHost.cjs +179 -0
- package/dist/cjs/syncHost.native.js +388 -0
- package/dist/cjs/syncHost.native.js.map +1 -0
- package/dist/cjs/syncHost.test.cjs +240 -0
- package/dist/cjs/syncHost.test.native.js +290 -0
- package/dist/cjs/syncHost.test.native.js.map +1 -0
- package/dist/cjs/zeroRunner.cjs +1 -1
- package/dist/cjs/zeroRunner.native.js +1 -1
- package/dist/esm/asyncTasks.mjs +26 -0
- package/dist/esm/asyncTasks.mjs.map +1 -0
- package/dist/esm/asyncTasks.native.js +56 -0
- package/dist/esm/asyncTasks.native.js.map +1 -0
- package/dist/esm/createZeroDirectServer.mjs +241 -0
- package/dist/esm/createZeroDirectServer.mjs.map +1 -0
- package/dist/esm/createZeroDirectServer.native.js +256 -0
- package/dist/esm/createZeroDirectServer.native.js.map +1 -0
- package/dist/esm/createZeroSQLiteServer.mjs +24 -0
- package/dist/esm/createZeroSQLiteServer.mjs.map +1 -0
- package/dist/esm/createZeroSQLiteServer.native.js +29 -0
- package/dist/esm/createZeroSQLiteServer.native.js.map +1 -0
- package/dist/esm/createZeroSQLiteServer.test.mjs +596 -0
- package/dist/esm/createZeroSQLiteServer.test.mjs.map +1 -0
- package/dist/esm/createZeroSQLiteServer.test.native.js +645 -0
- package/dist/esm/createZeroSQLiteServer.test.native.js.map +1 -0
- package/dist/esm/createZeroServer.mjs +57 -0
- package/dist/esm/createZeroServer.mjs.map +1 -0
- package/dist/esm/createZeroServer.native.js +65 -0
- package/dist/esm/createZeroServer.native.js.map +1 -0
- package/dist/esm/createZeroServer.test.mjs +379 -0
- package/dist/esm/createZeroServer.test.mjs.map +1 -0
- package/dist/esm/createZeroServer.test.native.js +446 -0
- package/dist/esm/createZeroServer.test.native.js.map +1 -0
- package/dist/esm/createZeroServerCore.mjs +186 -0
- package/dist/esm/createZeroServerCore.mjs.map +1 -0
- package/dist/esm/createZeroServerCore.native.js +327 -0
- package/dist/esm/createZeroServerCore.native.js.map +1 -0
- package/dist/esm/helpers/createMutators.mjs +3 -4
- package/dist/esm/helpers/createMutators.mjs.map +1 -1
- package/dist/esm/helpers/createMutators.native.js +3 -6
- package/dist/esm/helpers/createMutators.native.js.map +1 -1
- package/dist/esm/httpPull/auth.test.mjs +198 -0
- package/dist/esm/httpPull/auth.test.mjs.map +1 -0
- package/dist/esm/httpPull/auth.test.native.js +277 -0
- package/dist/esm/httpPull/auth.test.native.js.map +1 -0
- package/dist/esm/httpPull/churn.test.mjs +133 -0
- package/dist/esm/httpPull/churn.test.mjs.map +1 -0
- package/dist/esm/httpPull/churn.test.native.js +153 -0
- package/dist/esm/httpPull/churn.test.native.js.map +1 -0
- package/dist/esm/httpPull/completeAck.test.mjs +70 -0
- package/dist/esm/httpPull/completeAck.test.mjs.map +1 -0
- package/dist/esm/httpPull/completeAck.test.native.js +74 -0
- package/dist/esm/httpPull/completeAck.test.native.js.map +1 -0
- package/dist/esm/httpPull/fixtureServer.mjs +315 -0
- package/dist/esm/httpPull/fixtureServer.mjs.map +1 -0
- package/dist/esm/httpPull/fixtureServer.native.js +506 -0
- package/dist/esm/httpPull/fixtureServer.native.js.map +1 -0
- package/dist/esm/httpPull/integration.test.mjs +54 -0
- package/dist/esm/httpPull/integration.test.mjs.map +1 -0
- package/dist/esm/httpPull/integration.test.native.js +58 -0
- package/dist/esm/httpPull/integration.test.native.js.map +1 -0
- package/dist/esm/httpPull/rebase.test.mjs +361 -0
- package/dist/esm/httpPull/rebase.test.mjs.map +1 -0
- package/dist/esm/httpPull/rebase.test.native.js +418 -0
- package/dist/esm/httpPull/rebase.test.native.js.map +1 -0
- package/dist/esm/httpPull/relations.test.mjs +108 -0
- package/dist/esm/httpPull/relations.test.mjs.map +1 -0
- package/dist/esm/httpPull/relations.test.native.js +117 -0
- package/dist/esm/httpPull/relations.test.native.js.map +1 -0
- package/dist/esm/httpPull/testHarness.mjs +72 -0
- package/dist/esm/httpPull/testHarness.mjs.map +1 -0
- package/dist/esm/httpPull/testHarness.native.js +81 -0
- package/dist/esm/httpPull/testHarness.native.js.map +1 -0
- package/dist/esm/index.js +1 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/index.mjs +1 -0
- package/dist/esm/index.mjs.map +1 -1
- package/dist/esm/index.native.js +1 -0
- package/dist/esm/index.native.js.map +1 -1
- package/dist/esm/run.mjs +1 -1
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/run.native.js +1 -1
- package/dist/esm/server.mjs +4 -0
- package/dist/esm/server.mjs.map +1 -0
- package/dist/esm/server.native.js +2 -0
- package/dist/esm/server.native.js.map +1 -0
- package/dist/esm/sqliteRuntime.mjs +289 -0
- package/dist/esm/sqliteRuntime.mjs.map +1 -0
- package/dist/esm/sqliteRuntime.native.js +406 -0
- package/dist/esm/sqliteRuntime.native.js.map +1 -0
- package/dist/esm/state.mjs +1 -1
- package/dist/esm/state.native.js +1 -1
- package/dist/esm/syncHost.mjs +153 -0
- package/dist/esm/syncHost.mjs.map +1 -0
- package/dist/esm/syncHost.native.js +359 -0
- package/dist/esm/syncHost.native.js.map +1 -0
- package/dist/esm/syncHost.test.mjs +217 -0
- package/dist/esm/syncHost.test.mjs.map +1 -0
- package/dist/esm/syncHost.test.native.js +264 -0
- package/dist/esm/syncHost.test.native.js.map +1 -0
- package/dist/esm/zeroRunner.mjs +1 -1
- package/dist/esm/zeroRunner.mjs.map +1 -1
- package/dist/esm/zeroRunner.native.js +1 -1
- package/package.json +23 -8
- package/readme.md +65 -40
- package/src/asyncTasks.ts +36 -0
- package/src/createZeroClient.tsx +1 -1
- package/src/createZeroDirectServer.ts +411 -0
- package/src/createZeroSQLiteServer.test.ts +614 -0
- package/src/createZeroSQLiteServer.ts +56 -0
- package/src/createZeroServer.test.ts +421 -0
- package/src/createZeroServer.ts +105 -0
- package/src/createZeroServerCore.ts +296 -0
- package/src/helpers/createMutators.ts +2 -6
- package/src/httpPull/auth.test.ts +208 -0
- package/src/httpPull/churn.test.ts +147 -0
- package/src/httpPull/completeAck.test.ts +80 -0
- package/src/httpPull/fixtureServer.ts +391 -0
- package/src/httpPull/integration.test.ts +57 -0
- package/src/httpPull/rebase.test.ts +368 -0
- package/src/httpPull/relations.test.ts +135 -0
- package/src/httpPull/testHarness.ts +95 -0
- package/src/index.ts +1 -0
- package/src/run.ts +1 -1
- package/src/server.native.ts +3 -0
- package/src/server.ts +4 -0
- package/src/sqliteRuntime.ts +512 -0
- package/src/state.ts +1 -1
- package/src/syncHost.test.ts +223 -0
- package/src/syncHost.ts +294 -0
- package/src/types.ts +5 -0
- package/src/zeroRunner.ts +1 -1
- package/test-fixtures/server-return-types.ts +58 -21
- package/types/asyncTasks.d.ts +11 -0
- package/types/asyncTasks.d.ts.map +1 -0
- package/types/createZeroDirectServer.d.ts +80 -0
- package/types/createZeroDirectServer.d.ts.map +1 -0
- package/types/createZeroSQLiteServer.d.ts +12 -0
- package/types/createZeroSQLiteServer.d.ts.map +1 -0
- package/types/createZeroSQLiteServer.test.d.ts +2 -0
- package/types/createZeroSQLiteServer.test.d.ts.map +1 -0
- package/types/createZeroServer.d.ts +18 -0
- package/types/createZeroServer.d.ts.map +1 -0
- package/types/createZeroServer.test.d.ts +2 -0
- package/types/createZeroServer.test.d.ts.map +1 -0
- package/types/createZeroServerCore.d.ts +256 -0
- package/types/createZeroServerCore.d.ts.map +1 -0
- package/types/helpers/createMutators.d.ts +1 -2
- package/types/helpers/createMutators.d.ts.map +1 -1
- package/types/httpPull/auth.test.d.ts +2 -0
- package/types/httpPull/auth.test.d.ts.map +1 -0
- package/types/httpPull/churn.test.d.ts +2 -0
- package/types/httpPull/churn.test.d.ts.map +1 -0
- package/types/httpPull/completeAck.test.d.ts +2 -0
- package/types/httpPull/completeAck.test.d.ts.map +1 -0
- package/types/httpPull/fixtureSchema.d.ts +1 -1
- package/types/httpPull/fixtureServer.d.ts +14 -0
- package/types/httpPull/fixtureServer.d.ts.map +1 -0
- package/types/httpPull/integration.test.d.ts +2 -0
- package/types/httpPull/integration.test.d.ts.map +1 -0
- package/types/httpPull/rebase.test.d.ts +2 -0
- package/types/httpPull/rebase.test.d.ts.map +1 -0
- package/types/httpPull/relations.test.d.ts +2 -0
- package/types/httpPull/relations.test.d.ts.map +1 -0
- package/types/httpPull/testHarness.d.ts +32 -0
- package/types/httpPull/testHarness.d.ts.map +1 -0
- package/types/index.d.ts +1 -0
- package/types/index.d.ts.map +1 -1
- package/types/server.d.ts +5 -0
- package/types/server.d.ts.map +1 -0
- package/types/server.native.d.ts +1 -0
- package/types/server.native.d.ts.map +1 -0
- package/types/sqliteRuntime.d.ts +71 -0
- package/types/sqliteRuntime.d.ts.map +1 -0
- package/types/syncHost.d.ts +42 -0
- package/types/syncHost.d.ts.map +1 -0
- package/types/syncHost.test.d.ts +2 -0
- package/types/syncHost.test.d.ts.map +1 -0
- package/types/types.d.ts +4 -0
- package/types/types.d.ts.map +1 -1
- package/dist/cjs/orez.cjs +0 -227
- package/dist/cjs/orez.native.js +0 -314
- package/dist/cjs/orez.native.js.map +0 -1
- package/dist/cjs/orez.test.cjs +0 -211
- package/dist/cjs/orez.test.native.js +0 -226
- package/dist/cjs/orez.test.native.js.map +0 -1
- package/dist/esm/orez.mjs +0 -202
- package/dist/esm/orez.mjs.map +0 -1
- package/dist/esm/orez.native.js +0 -286
- package/dist/esm/orez.native.js.map +0 -1
- package/dist/esm/orez.test.mjs +0 -188
- package/dist/esm/orez.test.mjs.map +0 -1
- package/dist/esm/orez.test.native.js +0 -200
- package/dist/esm/orez.test.native.js.map +0 -1
- package/src/orez.test.ts +0 -195
- package/src/orez.ts +0 -331
- package/types/orez.d.ts +0 -53
- package/types/orez.d.ts.map +0 -1
- package/types/orez.test.d.ts +0 -2
- package/types/orez.test.d.ts.map +0 -1
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
import { describe, expect, test, vi } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { createZeroServer } from './createZeroServer'
|
|
4
|
+
import { zeroHttpFixtureSchema } from './httpPull/fixtureSchema'
|
|
5
|
+
|
|
6
|
+
class FakePgClient {
|
|
7
|
+
lastMutationID = 0
|
|
8
|
+
pendingLastMutationID: number | undefined
|
|
9
|
+
mutationResultInserts: unknown[][] = []
|
|
10
|
+
|
|
11
|
+
async query(sql: string, params: unknown[] = []) {
|
|
12
|
+
if (sql === 'BEGIN') {
|
|
13
|
+
this.pendingLastMutationID = undefined
|
|
14
|
+
return { rows: [] }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (sql === 'COMMIT') {
|
|
18
|
+
if (this.pendingLastMutationID !== undefined) {
|
|
19
|
+
this.lastMutationID = this.pendingLastMutationID
|
|
20
|
+
}
|
|
21
|
+
this.pendingLastMutationID = undefined
|
|
22
|
+
return { rows: [] }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (sql === 'ROLLBACK') {
|
|
26
|
+
this.pendingLastMutationID = undefined
|
|
27
|
+
return { rows: [] }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (sql.includes('.clients') && sql.includes('RETURNING "lastMutationID"')) {
|
|
31
|
+
const nextMutationID = this.lastMutationID + 1
|
|
32
|
+
this.pendingLastMutationID = nextMutationID
|
|
33
|
+
return { rows: [{ lastMutationID: nextMutationID }] }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (sql.includes('information_schema.columns')) {
|
|
37
|
+
return {
|
|
38
|
+
rows: [
|
|
39
|
+
...schemaRows('user', ['id', 'name']),
|
|
40
|
+
...schemaRows('project', ['id', 'ownerId', 'name']),
|
|
41
|
+
...schemaRows('member', ['id', 'projectId', 'userId']),
|
|
42
|
+
],
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (
|
|
47
|
+
sql.includes('INSERT INTO "zero_0"."mutations"') ||
|
|
48
|
+
sql.includes('INSERT INTO "zero_0".mutations')
|
|
49
|
+
) {
|
|
50
|
+
this.mutationResultInserts.push(params)
|
|
51
|
+
return { rows: [] }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
throw new Error(`unexpected fake pg query: ${sql}`)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function schemaRows(table: string, columns: string[]) {
|
|
59
|
+
return columns.map((column) => ({
|
|
60
|
+
schema: 'public',
|
|
61
|
+
table,
|
|
62
|
+
column,
|
|
63
|
+
dataType: 'text',
|
|
64
|
+
length: null,
|
|
65
|
+
precision: null,
|
|
66
|
+
scale: null,
|
|
67
|
+
typtype: 'b',
|
|
68
|
+
typename: 'text',
|
|
69
|
+
elemTyptype: null,
|
|
70
|
+
elemTypname: null,
|
|
71
|
+
}))
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function pushRequest(mutationIDs: number | number[]) {
|
|
75
|
+
const ids = Array.isArray(mutationIDs) ? mutationIDs : [mutationIDs]
|
|
76
|
+
return new Request('http://localhost/push?schema=zero_0&appID=zero', {
|
|
77
|
+
method: 'POST',
|
|
78
|
+
headers: { 'content-type': 'application/json' },
|
|
79
|
+
body: JSON.stringify({
|
|
80
|
+
clientGroupID: 'cg1',
|
|
81
|
+
mutations: ids.map((mutationID) => ({
|
|
82
|
+
type: 'custom',
|
|
83
|
+
id: mutationID,
|
|
84
|
+
clientID: 'client-a',
|
|
85
|
+
name: 'project.touch',
|
|
86
|
+
args: [{}],
|
|
87
|
+
timestamp: 1,
|
|
88
|
+
})),
|
|
89
|
+
pushVersion: 1,
|
|
90
|
+
timestamp: 1,
|
|
91
|
+
requestID: `req-${ids.join('-')}`,
|
|
92
|
+
}),
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
describe('createZeroServer mutation recovery', () => {
|
|
97
|
+
test('discards async tasks from a rolled-back PostgreSQL mutation', async () => {
|
|
98
|
+
const pg = new FakePgClient()
|
|
99
|
+
let effectRuns = 0
|
|
100
|
+
const consoleWarn = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
101
|
+
const zeroServer = createZeroServer({
|
|
102
|
+
schema: zeroHttpFixtureSchema,
|
|
103
|
+
models: {
|
|
104
|
+
project: {
|
|
105
|
+
mutate: {
|
|
106
|
+
touch: async ({ server }) => {
|
|
107
|
+
server?.enqueueTask(async () => {
|
|
108
|
+
effectRuns++
|
|
109
|
+
})
|
|
110
|
+
throw new Error('rejected')
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
createServerActions: () => ({}),
|
|
116
|
+
pool: pg as any,
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
const result = await zeroServer.handleMutationRequest({
|
|
120
|
+
authData: null,
|
|
121
|
+
request: pushRequest(1),
|
|
122
|
+
skipAsyncTasks: true,
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
expect((result.response as any).mutations[0].result).toMatchObject({
|
|
126
|
+
error: 'app',
|
|
127
|
+
message: 'rejected',
|
|
128
|
+
})
|
|
129
|
+
expect(result.asyncTasks).toEqual([])
|
|
130
|
+
expect(effectRuns).toBe(0)
|
|
131
|
+
expect(consoleWarn).toHaveBeenCalledWith(
|
|
132
|
+
'PushProcessor',
|
|
133
|
+
expect.stringContaining('Error processing mutation'),
|
|
134
|
+
expect.stringContaining('rejected'),
|
|
135
|
+
)
|
|
136
|
+
consoleWarn.mockRestore()
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
test('awaits barrier tasks before resolving the push response', async () => {
|
|
140
|
+
const pg = new FakePgClient()
|
|
141
|
+
let releaseBarrier!: () => void
|
|
142
|
+
let markBarrierStarted!: () => void
|
|
143
|
+
const barrierStarted = new Promise<void>((resolve) => {
|
|
144
|
+
markBarrierStarted = resolve
|
|
145
|
+
})
|
|
146
|
+
const barrier = new Promise<void>((resolve) => {
|
|
147
|
+
releaseBarrier = resolve
|
|
148
|
+
})
|
|
149
|
+
const zeroServer = createZeroServer({
|
|
150
|
+
schema: zeroHttpFixtureSchema,
|
|
151
|
+
models: {
|
|
152
|
+
project: {
|
|
153
|
+
mutate: {
|
|
154
|
+
touch: async ({ server }) => {
|
|
155
|
+
server?.enqueueTask(
|
|
156
|
+
async () => {
|
|
157
|
+
markBarrierStarted()
|
|
158
|
+
await barrier
|
|
159
|
+
},
|
|
160
|
+
{ barrier: true },
|
|
161
|
+
)
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
createServerActions: () => ({}),
|
|
167
|
+
pool: pg as any,
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
let responseResolved = false
|
|
171
|
+
const resultPromise = zeroServer
|
|
172
|
+
.handleMutationRequest({
|
|
173
|
+
authData: null,
|
|
174
|
+
request: pushRequest(1),
|
|
175
|
+
})
|
|
176
|
+
.then((result) => {
|
|
177
|
+
responseResolved = true
|
|
178
|
+
return result
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
await barrierStarted
|
|
182
|
+
await Promise.resolve()
|
|
183
|
+
expect(responseResolved).toBe(false)
|
|
184
|
+
|
|
185
|
+
releaseBarrier()
|
|
186
|
+
const result = await resultPromise
|
|
187
|
+
expect((result.response as any).mutations[0].result).toEqual({})
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
test('does not fail the push when a background task rejects', async () => {
|
|
191
|
+
const pg = new FakePgClient()
|
|
192
|
+
const error = new Error('background failed')
|
|
193
|
+
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
194
|
+
const zeroServer = createZeroServer({
|
|
195
|
+
schema: zeroHttpFixtureSchema,
|
|
196
|
+
models: {
|
|
197
|
+
project: {
|
|
198
|
+
mutate: {
|
|
199
|
+
touch: async ({ server }) => {
|
|
200
|
+
server?.enqueueTask(async () => {
|
|
201
|
+
throw error
|
|
202
|
+
})
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
createServerActions: () => ({}),
|
|
208
|
+
pool: pg as any,
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
const result = await zeroServer.handleMutationRequest({
|
|
212
|
+
authData: null,
|
|
213
|
+
request: pushRequest(1),
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
expect((result.response as any).mutations[0].result).toEqual({})
|
|
217
|
+
await vi.waitFor(() => {
|
|
218
|
+
expect(consoleError).toHaveBeenCalledWith('[on-zero] async task failed', error)
|
|
219
|
+
})
|
|
220
|
+
consoleError.mockRestore()
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
test('registers a promise that settles every background task with the request scheduler', async () => {
|
|
224
|
+
const pg = new FakePgClient()
|
|
225
|
+
const error = new Error('background failed')
|
|
226
|
+
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
227
|
+
let releaseSibling!: () => void
|
|
228
|
+
const siblingGate = new Promise<void>((resolve) => {
|
|
229
|
+
releaseSibling = resolve
|
|
230
|
+
})
|
|
231
|
+
let siblingFinished = false
|
|
232
|
+
let scheduled: Promise<void> | undefined
|
|
233
|
+
const zeroServer = createZeroServer({
|
|
234
|
+
schema: zeroHttpFixtureSchema,
|
|
235
|
+
models: {
|
|
236
|
+
project: {
|
|
237
|
+
mutate: {
|
|
238
|
+
touch: async ({ server }) => {
|
|
239
|
+
server?.enqueueTask(async () => {
|
|
240
|
+
throw error
|
|
241
|
+
})
|
|
242
|
+
server?.enqueueTask(async () => {
|
|
243
|
+
await siblingGate
|
|
244
|
+
siblingFinished = true
|
|
245
|
+
})
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
createServerActions: () => ({}),
|
|
251
|
+
pool: pg as any,
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
const result = await zeroServer.handleMutationRequest({
|
|
255
|
+
authData: null,
|
|
256
|
+
request: pushRequest(1),
|
|
257
|
+
scheduleBackgroundTask: (promise) => {
|
|
258
|
+
scheduled = promise
|
|
259
|
+
},
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
expect((result.response as any).mutations[0].result).toEqual({})
|
|
263
|
+
expect(scheduled).toBeInstanceOf(Promise)
|
|
264
|
+
expect(siblingFinished).toBe(false)
|
|
265
|
+
|
|
266
|
+
let scheduledSettled = false
|
|
267
|
+
void scheduled!.then(() => {
|
|
268
|
+
scheduledSettled = true
|
|
269
|
+
})
|
|
270
|
+
await Promise.resolve()
|
|
271
|
+
expect(scheduledSettled).toBe(false)
|
|
272
|
+
|
|
273
|
+
releaseSibling()
|
|
274
|
+
await scheduled
|
|
275
|
+
|
|
276
|
+
expect(siblingFinished).toBe(true)
|
|
277
|
+
expect(consoleError).toHaveBeenCalledWith('[on-zero] async task failed', error)
|
|
278
|
+
consoleError.mockRestore()
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
test('awaits background tasks when the request has no scheduler', async () => {
|
|
282
|
+
const pg = new FakePgClient()
|
|
283
|
+
let markTaskStarted!: () => void
|
|
284
|
+
const taskStarted = new Promise<void>((resolve) => {
|
|
285
|
+
markTaskStarted = resolve
|
|
286
|
+
})
|
|
287
|
+
let releaseTask!: () => void
|
|
288
|
+
const taskGate = new Promise<void>((resolve) => {
|
|
289
|
+
releaseTask = resolve
|
|
290
|
+
})
|
|
291
|
+
const zeroServer = createZeroServer({
|
|
292
|
+
schema: zeroHttpFixtureSchema,
|
|
293
|
+
models: {
|
|
294
|
+
project: {
|
|
295
|
+
mutate: {
|
|
296
|
+
touch: async ({ server }) => {
|
|
297
|
+
server?.enqueueTask(async () => {
|
|
298
|
+
markTaskStarted()
|
|
299
|
+
await taskGate
|
|
300
|
+
})
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
},
|
|
304
|
+
},
|
|
305
|
+
createServerActions: () => ({}),
|
|
306
|
+
pool: pg as any,
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
let responseResolved = false
|
|
310
|
+
const resultPromise = zeroServer
|
|
311
|
+
.handleMutationRequest({
|
|
312
|
+
authData: null,
|
|
313
|
+
request: pushRequest(1),
|
|
314
|
+
})
|
|
315
|
+
.then((result) => {
|
|
316
|
+
responseResolved = true
|
|
317
|
+
return result
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
await taskStarted
|
|
321
|
+
await Promise.resolve()
|
|
322
|
+
expect(responseResolved).toBe(false)
|
|
323
|
+
|
|
324
|
+
releaseTask()
|
|
325
|
+
const result = await resultPromise
|
|
326
|
+
expect((result.response as any).mutations[0].result).toEqual({})
|
|
327
|
+
})
|
|
328
|
+
|
|
329
|
+
test('persists alreadyProcessed duplicate results for zero-cache recovery rows', async () => {
|
|
330
|
+
const pg = new FakePgClient()
|
|
331
|
+
const consoleWarn = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
332
|
+
const zeroServer = createZeroServer({
|
|
333
|
+
schema: zeroHttpFixtureSchema,
|
|
334
|
+
models: {
|
|
335
|
+
project: {
|
|
336
|
+
mutate: {
|
|
337
|
+
touch: async () => {},
|
|
338
|
+
},
|
|
339
|
+
},
|
|
340
|
+
},
|
|
341
|
+
createServerActions: () => ({}),
|
|
342
|
+
pool: pg as any,
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
const first = await zeroServer.handleMutationRequest({
|
|
346
|
+
authData: null,
|
|
347
|
+
request: pushRequest(1),
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
expect((first.response as any).mutations[0].result).toEqual({})
|
|
351
|
+
expect(pg.lastMutationID).toBe(1)
|
|
352
|
+
expect(pg.mutationResultInserts).toEqual([])
|
|
353
|
+
|
|
354
|
+
const duplicate = await zeroServer.handleMutationRequest({
|
|
355
|
+
authData: null,
|
|
356
|
+
request: pushRequest(1),
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
const duplicateResult = (duplicate.response as any).mutations[0].result
|
|
360
|
+
expect(duplicateResult.error).toBe('alreadyProcessed')
|
|
361
|
+
expect(pg.lastMutationID).toBe(1)
|
|
362
|
+
expect(pg.mutationResultInserts).toEqual([
|
|
363
|
+
[
|
|
364
|
+
'cg1',
|
|
365
|
+
'client-a',
|
|
366
|
+
1,
|
|
367
|
+
JSON.stringify({
|
|
368
|
+
error: 'alreadyProcessed',
|
|
369
|
+
details: duplicateResult.details,
|
|
370
|
+
}),
|
|
371
|
+
],
|
|
372
|
+
])
|
|
373
|
+
expect(consoleWarn).not.toHaveBeenCalled()
|
|
374
|
+
consoleWarn.mockRestore()
|
|
375
|
+
})
|
|
376
|
+
|
|
377
|
+
test('persists only alreadyProcessed results in a mixed recovery push', async () => {
|
|
378
|
+
const pg = new FakePgClient()
|
|
379
|
+
const zeroServer = createZeroServer({
|
|
380
|
+
schema: zeroHttpFixtureSchema,
|
|
381
|
+
models: {
|
|
382
|
+
project: {
|
|
383
|
+
mutate: {
|
|
384
|
+
touch: async () => {},
|
|
385
|
+
},
|
|
386
|
+
},
|
|
387
|
+
},
|
|
388
|
+
createServerActions: () => ({}),
|
|
389
|
+
pool: pg as any,
|
|
390
|
+
})
|
|
391
|
+
|
|
392
|
+
await zeroServer.handleMutationRequest({
|
|
393
|
+
authData: null,
|
|
394
|
+
request: pushRequest(1),
|
|
395
|
+
})
|
|
396
|
+
|
|
397
|
+
const mixed = await zeroServer.handleMutationRequest({
|
|
398
|
+
authData: null,
|
|
399
|
+
request: pushRequest([1, 2]),
|
|
400
|
+
})
|
|
401
|
+
|
|
402
|
+
const results = (mixed.response as any).mutations.map((mutation: any) => ({
|
|
403
|
+
id: mutation.id.id,
|
|
404
|
+
result: mutation.result,
|
|
405
|
+
}))
|
|
406
|
+
expect(results[0].result.error).toBe('alreadyProcessed')
|
|
407
|
+
expect(results[1]).toEqual({ id: 2, result: {} })
|
|
408
|
+
expect(pg.lastMutationID).toBe(2)
|
|
409
|
+
expect(pg.mutationResultInserts).toEqual([
|
|
410
|
+
[
|
|
411
|
+
'cg1',
|
|
412
|
+
'client-a',
|
|
413
|
+
1,
|
|
414
|
+
JSON.stringify({
|
|
415
|
+
error: 'alreadyProcessed',
|
|
416
|
+
details: results[0].result.details,
|
|
417
|
+
}),
|
|
418
|
+
],
|
|
419
|
+
])
|
|
420
|
+
})
|
|
421
|
+
})
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { zeroNodePg } from '@rocicorp/zero/server/adapters/pg'
|
|
2
|
+
import { assertString } from '@take-out/helpers'
|
|
3
|
+
import { Pool } from 'pg'
|
|
4
|
+
|
|
5
|
+
import { createZeroServerCore } from './createZeroServerCore'
|
|
6
|
+
|
|
7
|
+
import type {
|
|
8
|
+
PersistAlreadyProcessedArgs,
|
|
9
|
+
ZeroServer,
|
|
10
|
+
ZeroServerCommonOptions,
|
|
11
|
+
ZeroServerDatabase,
|
|
12
|
+
} from './createZeroServerCore'
|
|
13
|
+
import type { GenericModels } from './types'
|
|
14
|
+
import type { Schema as ZeroSchema } from '@rocicorp/zero'
|
|
15
|
+
import type { NodePgTransaction } from '@rocicorp/zero/server/adapters/pg'
|
|
16
|
+
|
|
17
|
+
export type {
|
|
18
|
+
BackgroundTaskScheduler,
|
|
19
|
+
HandleMutationRequestOptions,
|
|
20
|
+
ValidateMutationArgs,
|
|
21
|
+
ValidateMutationFn,
|
|
22
|
+
ValidateQueryArgs,
|
|
23
|
+
ValidateQueryFn,
|
|
24
|
+
} from './createZeroServerCore'
|
|
25
|
+
|
|
26
|
+
export function createZeroServer<
|
|
27
|
+
Schema extends ZeroSchema,
|
|
28
|
+
Models extends GenericModels,
|
|
29
|
+
ServerActions extends Record<string, unknown>,
|
|
30
|
+
>({
|
|
31
|
+
database,
|
|
32
|
+
pool: externalPool,
|
|
33
|
+
...options
|
|
34
|
+
}: ZeroServerCommonOptions<Schema, Models, ServerActions> & {
|
|
35
|
+
/**
|
|
36
|
+
* Optional pre-built Pool-like object, such as a PGlite wrapper.
|
|
37
|
+
* When provided, database is not required.
|
|
38
|
+
*/
|
|
39
|
+
pool?: NodePgTransaction
|
|
40
|
+
/**
|
|
41
|
+
* The PostgreSQL connection string.
|
|
42
|
+
* Not required when pool is provided.
|
|
43
|
+
*/
|
|
44
|
+
database?: string
|
|
45
|
+
}): ZeroServer<Schema, Models, ServerActions> {
|
|
46
|
+
let pool: NodePgTransaction
|
|
47
|
+
if (externalPool) {
|
|
48
|
+
pool = externalPool
|
|
49
|
+
} else {
|
|
50
|
+
const dbString = assertString(database, 'createZeroServer "database"')
|
|
51
|
+
const pgPool = new Pool({
|
|
52
|
+
connectionString: dbString,
|
|
53
|
+
ssl: dbString.includes('sslmode=require')
|
|
54
|
+
? { rejectUnauthorized: false }
|
|
55
|
+
: undefined,
|
|
56
|
+
})
|
|
57
|
+
pgPool.on('error', (error) => {
|
|
58
|
+
console.error('[on-zero] pool error', error.message)
|
|
59
|
+
})
|
|
60
|
+
pgPool.on('connect', (client) => {
|
|
61
|
+
client.on('error', (error) => {
|
|
62
|
+
console.error('[on-zero] client error', error.message)
|
|
63
|
+
})
|
|
64
|
+
})
|
|
65
|
+
pool = pgPool
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const databaseProvider = zeroNodePg(options.schema, pool)
|
|
69
|
+
|
|
70
|
+
return createZeroServerCore({
|
|
71
|
+
...options,
|
|
72
|
+
createDatabase: () => databaseProvider as unknown as ZeroServerDatabase,
|
|
73
|
+
persistAlreadyProcessed: (args) => persistAlreadyProcessedMutationResults(pool, args),
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function persistAlreadyProcessedMutationResults(
|
|
78
|
+
pool: NodePgTransaction,
|
|
79
|
+
{ clientGroupID, mutations, upstreamSchema }: PersistAlreadyProcessedArgs,
|
|
80
|
+
) {
|
|
81
|
+
const table = `${quotePgIdentifier(upstreamSchema)}."mutations"`
|
|
82
|
+
const values = mutations
|
|
83
|
+
.map((_, index) => {
|
|
84
|
+
const offset = index * 4
|
|
85
|
+
return `($${offset + 1}, $${offset + 2}, $${offset + 3}, $${offset + 4}::json)`
|
|
86
|
+
})
|
|
87
|
+
.join(', ')
|
|
88
|
+
const params = mutations.flatMap((mutation) => [
|
|
89
|
+
clientGroupID,
|
|
90
|
+
mutation.id.clientID,
|
|
91
|
+
mutation.id.id,
|
|
92
|
+
JSON.stringify(mutation.result),
|
|
93
|
+
])
|
|
94
|
+
|
|
95
|
+
await pool.query(
|
|
96
|
+
`INSERT INTO ${table} ("clientGroupID", "clientID", "mutationID", "result")
|
|
97
|
+
VALUES ${values}
|
|
98
|
+
ON CONFLICT ("clientGroupID", "clientID", "mutationID") DO NOTHING`,
|
|
99
|
+
params,
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function quotePgIdentifier(value: string) {
|
|
104
|
+
return `"${value.replaceAll('"', '""')}"`
|
|
105
|
+
}
|