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.
Files changed (260) hide show
  1. package/dist/cjs/asyncTasks.cjs +51 -0
  2. package/dist/cjs/asyncTasks.native.js +84 -0
  3. package/dist/cjs/asyncTasks.native.js.map +1 -0
  4. package/dist/cjs/createZeroDirectServer.cjs +269 -0
  5. package/dist/cjs/createZeroDirectServer.native.js +287 -0
  6. package/dist/cjs/createZeroDirectServer.native.js.map +1 -0
  7. package/dist/cjs/createZeroSQLiteServer.cjs +50 -0
  8. package/dist/cjs/createZeroSQLiteServer.native.js +58 -0
  9. package/dist/cjs/createZeroSQLiteServer.native.js.map +1 -0
  10. package/dist/cjs/createZeroSQLiteServer.test.cjs +595 -0
  11. package/dist/cjs/createZeroSQLiteServer.test.native.js +647 -0
  12. package/dist/cjs/createZeroSQLiteServer.test.native.js.map +1 -0
  13. package/dist/cjs/createZeroServer.cjs +82 -0
  14. package/dist/cjs/createZeroServer.native.js +93 -0
  15. package/dist/cjs/createZeroServer.native.js.map +1 -0
  16. package/dist/cjs/createZeroServer.test.cjs +378 -0
  17. package/dist/cjs/createZeroServer.test.native.js +448 -0
  18. package/dist/cjs/createZeroServer.test.native.js.map +1 -0
  19. package/dist/cjs/createZeroServerCore.cjs +211 -0
  20. package/dist/cjs/createZeroServerCore.native.js +355 -0
  21. package/dist/cjs/createZeroServerCore.native.js.map +1 -0
  22. package/dist/cjs/helpers/createMutators.cjs +3 -4
  23. package/dist/cjs/helpers/createMutators.native.js +3 -6
  24. package/dist/cjs/helpers/createMutators.native.js.map +1 -1
  25. package/dist/cjs/httpPull/auth.test.cjs +197 -0
  26. package/dist/cjs/httpPull/auth.test.native.js +279 -0
  27. package/dist/cjs/httpPull/auth.test.native.js.map +1 -0
  28. package/dist/cjs/httpPull/churn.test.cjs +132 -0
  29. package/dist/cjs/httpPull/churn.test.native.js +155 -0
  30. package/dist/cjs/httpPull/churn.test.native.js.map +1 -0
  31. package/dist/cjs/httpPull/completeAck.test.cjs +69 -0
  32. package/dist/cjs/httpPull/completeAck.test.native.js +76 -0
  33. package/dist/cjs/httpPull/completeAck.test.native.js.map +1 -0
  34. package/dist/cjs/httpPull/fixtureServer.cjs +340 -0
  35. package/dist/cjs/httpPull/fixtureServer.native.js +534 -0
  36. package/dist/cjs/httpPull/fixtureServer.native.js.map +1 -0
  37. package/dist/cjs/httpPull/integration.test.cjs +53 -0
  38. package/dist/cjs/httpPull/integration.test.native.js +60 -0
  39. package/dist/cjs/httpPull/integration.test.native.js.map +1 -0
  40. package/dist/cjs/httpPull/rebase.test.cjs +360 -0
  41. package/dist/cjs/httpPull/rebase.test.native.js +420 -0
  42. package/dist/cjs/httpPull/rebase.test.native.js.map +1 -0
  43. package/dist/cjs/httpPull/relations.test.cjs +107 -0
  44. package/dist/cjs/httpPull/relations.test.native.js +119 -0
  45. package/dist/cjs/httpPull/relations.test.native.js.map +1 -0
  46. package/dist/cjs/httpPull/testHarness.cjs +100 -0
  47. package/dist/cjs/httpPull/testHarness.native.js +112 -0
  48. package/dist/cjs/httpPull/testHarness.native.js.map +1 -0
  49. package/dist/cjs/index.cjs +1 -0
  50. package/dist/cjs/index.native.js +1 -0
  51. package/dist/cjs/index.native.js.map +1 -1
  52. package/dist/cjs/run.cjs +1 -1
  53. package/dist/cjs/run.native.js +1 -1
  54. package/dist/cjs/server.cjs +22 -0
  55. package/dist/cjs/server.native.js +4 -0
  56. package/dist/cjs/server.native.js.map +1 -0
  57. package/dist/cjs/sqliteRuntime.cjs +318 -0
  58. package/dist/cjs/sqliteRuntime.native.js +438 -0
  59. package/dist/cjs/sqliteRuntime.native.js.map +1 -0
  60. package/dist/cjs/state.cjs +1 -1
  61. package/dist/cjs/state.native.js +1 -1
  62. package/dist/cjs/syncHost.cjs +179 -0
  63. package/dist/cjs/syncHost.native.js +388 -0
  64. package/dist/cjs/syncHost.native.js.map +1 -0
  65. package/dist/cjs/syncHost.test.cjs +240 -0
  66. package/dist/cjs/syncHost.test.native.js +290 -0
  67. package/dist/cjs/syncHost.test.native.js.map +1 -0
  68. package/dist/cjs/zeroRunner.cjs +1 -1
  69. package/dist/cjs/zeroRunner.native.js +1 -1
  70. package/dist/esm/asyncTasks.mjs +26 -0
  71. package/dist/esm/asyncTasks.mjs.map +1 -0
  72. package/dist/esm/asyncTasks.native.js +56 -0
  73. package/dist/esm/asyncTasks.native.js.map +1 -0
  74. package/dist/esm/createZeroDirectServer.mjs +241 -0
  75. package/dist/esm/createZeroDirectServer.mjs.map +1 -0
  76. package/dist/esm/createZeroDirectServer.native.js +256 -0
  77. package/dist/esm/createZeroDirectServer.native.js.map +1 -0
  78. package/dist/esm/createZeroSQLiteServer.mjs +24 -0
  79. package/dist/esm/createZeroSQLiteServer.mjs.map +1 -0
  80. package/dist/esm/createZeroSQLiteServer.native.js +29 -0
  81. package/dist/esm/createZeroSQLiteServer.native.js.map +1 -0
  82. package/dist/esm/createZeroSQLiteServer.test.mjs +596 -0
  83. package/dist/esm/createZeroSQLiteServer.test.mjs.map +1 -0
  84. package/dist/esm/createZeroSQLiteServer.test.native.js +645 -0
  85. package/dist/esm/createZeroSQLiteServer.test.native.js.map +1 -0
  86. package/dist/esm/createZeroServer.mjs +57 -0
  87. package/dist/esm/createZeroServer.mjs.map +1 -0
  88. package/dist/esm/createZeroServer.native.js +65 -0
  89. package/dist/esm/createZeroServer.native.js.map +1 -0
  90. package/dist/esm/createZeroServer.test.mjs +379 -0
  91. package/dist/esm/createZeroServer.test.mjs.map +1 -0
  92. package/dist/esm/createZeroServer.test.native.js +446 -0
  93. package/dist/esm/createZeroServer.test.native.js.map +1 -0
  94. package/dist/esm/createZeroServerCore.mjs +186 -0
  95. package/dist/esm/createZeroServerCore.mjs.map +1 -0
  96. package/dist/esm/createZeroServerCore.native.js +327 -0
  97. package/dist/esm/createZeroServerCore.native.js.map +1 -0
  98. package/dist/esm/helpers/createMutators.mjs +3 -4
  99. package/dist/esm/helpers/createMutators.mjs.map +1 -1
  100. package/dist/esm/helpers/createMutators.native.js +3 -6
  101. package/dist/esm/helpers/createMutators.native.js.map +1 -1
  102. package/dist/esm/httpPull/auth.test.mjs +198 -0
  103. package/dist/esm/httpPull/auth.test.mjs.map +1 -0
  104. package/dist/esm/httpPull/auth.test.native.js +277 -0
  105. package/dist/esm/httpPull/auth.test.native.js.map +1 -0
  106. package/dist/esm/httpPull/churn.test.mjs +133 -0
  107. package/dist/esm/httpPull/churn.test.mjs.map +1 -0
  108. package/dist/esm/httpPull/churn.test.native.js +153 -0
  109. package/dist/esm/httpPull/churn.test.native.js.map +1 -0
  110. package/dist/esm/httpPull/completeAck.test.mjs +70 -0
  111. package/dist/esm/httpPull/completeAck.test.mjs.map +1 -0
  112. package/dist/esm/httpPull/completeAck.test.native.js +74 -0
  113. package/dist/esm/httpPull/completeAck.test.native.js.map +1 -0
  114. package/dist/esm/httpPull/fixtureServer.mjs +315 -0
  115. package/dist/esm/httpPull/fixtureServer.mjs.map +1 -0
  116. package/dist/esm/httpPull/fixtureServer.native.js +506 -0
  117. package/dist/esm/httpPull/fixtureServer.native.js.map +1 -0
  118. package/dist/esm/httpPull/integration.test.mjs +54 -0
  119. package/dist/esm/httpPull/integration.test.mjs.map +1 -0
  120. package/dist/esm/httpPull/integration.test.native.js +58 -0
  121. package/dist/esm/httpPull/integration.test.native.js.map +1 -0
  122. package/dist/esm/httpPull/rebase.test.mjs +361 -0
  123. package/dist/esm/httpPull/rebase.test.mjs.map +1 -0
  124. package/dist/esm/httpPull/rebase.test.native.js +418 -0
  125. package/dist/esm/httpPull/rebase.test.native.js.map +1 -0
  126. package/dist/esm/httpPull/relations.test.mjs +108 -0
  127. package/dist/esm/httpPull/relations.test.mjs.map +1 -0
  128. package/dist/esm/httpPull/relations.test.native.js +117 -0
  129. package/dist/esm/httpPull/relations.test.native.js.map +1 -0
  130. package/dist/esm/httpPull/testHarness.mjs +72 -0
  131. package/dist/esm/httpPull/testHarness.mjs.map +1 -0
  132. package/dist/esm/httpPull/testHarness.native.js +81 -0
  133. package/dist/esm/httpPull/testHarness.native.js.map +1 -0
  134. package/dist/esm/index.js +1 -0
  135. package/dist/esm/index.js.map +1 -1
  136. package/dist/esm/index.mjs +1 -0
  137. package/dist/esm/index.mjs.map +1 -1
  138. package/dist/esm/index.native.js +1 -0
  139. package/dist/esm/index.native.js.map +1 -1
  140. package/dist/esm/run.mjs +1 -1
  141. package/dist/esm/run.mjs.map +1 -1
  142. package/dist/esm/run.native.js +1 -1
  143. package/dist/esm/server.mjs +4 -0
  144. package/dist/esm/server.mjs.map +1 -0
  145. package/dist/esm/server.native.js +2 -0
  146. package/dist/esm/server.native.js.map +1 -0
  147. package/dist/esm/sqliteRuntime.mjs +289 -0
  148. package/dist/esm/sqliteRuntime.mjs.map +1 -0
  149. package/dist/esm/sqliteRuntime.native.js +406 -0
  150. package/dist/esm/sqliteRuntime.native.js.map +1 -0
  151. package/dist/esm/state.mjs +1 -1
  152. package/dist/esm/state.native.js +1 -1
  153. package/dist/esm/syncHost.mjs +153 -0
  154. package/dist/esm/syncHost.mjs.map +1 -0
  155. package/dist/esm/syncHost.native.js +359 -0
  156. package/dist/esm/syncHost.native.js.map +1 -0
  157. package/dist/esm/syncHost.test.mjs +217 -0
  158. package/dist/esm/syncHost.test.mjs.map +1 -0
  159. package/dist/esm/syncHost.test.native.js +264 -0
  160. package/dist/esm/syncHost.test.native.js.map +1 -0
  161. package/dist/esm/zeroRunner.mjs +1 -1
  162. package/dist/esm/zeroRunner.mjs.map +1 -1
  163. package/dist/esm/zeroRunner.native.js +1 -1
  164. package/package.json +23 -8
  165. package/readme.md +65 -40
  166. package/src/asyncTasks.ts +36 -0
  167. package/src/createZeroClient.tsx +1 -1
  168. package/src/createZeroDirectServer.ts +411 -0
  169. package/src/createZeroSQLiteServer.test.ts +614 -0
  170. package/src/createZeroSQLiteServer.ts +56 -0
  171. package/src/createZeroServer.test.ts +421 -0
  172. package/src/createZeroServer.ts +105 -0
  173. package/src/createZeroServerCore.ts +296 -0
  174. package/src/helpers/createMutators.ts +2 -6
  175. package/src/httpPull/auth.test.ts +208 -0
  176. package/src/httpPull/churn.test.ts +147 -0
  177. package/src/httpPull/completeAck.test.ts +80 -0
  178. package/src/httpPull/fixtureServer.ts +391 -0
  179. package/src/httpPull/integration.test.ts +57 -0
  180. package/src/httpPull/rebase.test.ts +368 -0
  181. package/src/httpPull/relations.test.ts +135 -0
  182. package/src/httpPull/testHarness.ts +95 -0
  183. package/src/index.ts +1 -0
  184. package/src/run.ts +1 -1
  185. package/src/server.native.ts +3 -0
  186. package/src/server.ts +4 -0
  187. package/src/sqliteRuntime.ts +512 -0
  188. package/src/state.ts +1 -1
  189. package/src/syncHost.test.ts +223 -0
  190. package/src/syncHost.ts +294 -0
  191. package/src/types.ts +5 -0
  192. package/src/zeroRunner.ts +1 -1
  193. package/test-fixtures/server-return-types.ts +58 -21
  194. package/types/asyncTasks.d.ts +11 -0
  195. package/types/asyncTasks.d.ts.map +1 -0
  196. package/types/createZeroDirectServer.d.ts +80 -0
  197. package/types/createZeroDirectServer.d.ts.map +1 -0
  198. package/types/createZeroSQLiteServer.d.ts +12 -0
  199. package/types/createZeroSQLiteServer.d.ts.map +1 -0
  200. package/types/createZeroSQLiteServer.test.d.ts +2 -0
  201. package/types/createZeroSQLiteServer.test.d.ts.map +1 -0
  202. package/types/createZeroServer.d.ts +18 -0
  203. package/types/createZeroServer.d.ts.map +1 -0
  204. package/types/createZeroServer.test.d.ts +2 -0
  205. package/types/createZeroServer.test.d.ts.map +1 -0
  206. package/types/createZeroServerCore.d.ts +256 -0
  207. package/types/createZeroServerCore.d.ts.map +1 -0
  208. package/types/helpers/createMutators.d.ts +1 -2
  209. package/types/helpers/createMutators.d.ts.map +1 -1
  210. package/types/httpPull/auth.test.d.ts +2 -0
  211. package/types/httpPull/auth.test.d.ts.map +1 -0
  212. package/types/httpPull/churn.test.d.ts +2 -0
  213. package/types/httpPull/churn.test.d.ts.map +1 -0
  214. package/types/httpPull/completeAck.test.d.ts +2 -0
  215. package/types/httpPull/completeAck.test.d.ts.map +1 -0
  216. package/types/httpPull/fixtureSchema.d.ts +1 -1
  217. package/types/httpPull/fixtureServer.d.ts +14 -0
  218. package/types/httpPull/fixtureServer.d.ts.map +1 -0
  219. package/types/httpPull/integration.test.d.ts +2 -0
  220. package/types/httpPull/integration.test.d.ts.map +1 -0
  221. package/types/httpPull/rebase.test.d.ts +2 -0
  222. package/types/httpPull/rebase.test.d.ts.map +1 -0
  223. package/types/httpPull/relations.test.d.ts +2 -0
  224. package/types/httpPull/relations.test.d.ts.map +1 -0
  225. package/types/httpPull/testHarness.d.ts +32 -0
  226. package/types/httpPull/testHarness.d.ts.map +1 -0
  227. package/types/index.d.ts +1 -0
  228. package/types/index.d.ts.map +1 -1
  229. package/types/server.d.ts +5 -0
  230. package/types/server.d.ts.map +1 -0
  231. package/types/server.native.d.ts +1 -0
  232. package/types/server.native.d.ts.map +1 -0
  233. package/types/sqliteRuntime.d.ts +71 -0
  234. package/types/sqliteRuntime.d.ts.map +1 -0
  235. package/types/syncHost.d.ts +42 -0
  236. package/types/syncHost.d.ts.map +1 -0
  237. package/types/syncHost.test.d.ts +2 -0
  238. package/types/syncHost.test.d.ts.map +1 -0
  239. package/types/types.d.ts +4 -0
  240. package/types/types.d.ts.map +1 -1
  241. package/dist/cjs/orez.cjs +0 -227
  242. package/dist/cjs/orez.native.js +0 -314
  243. package/dist/cjs/orez.native.js.map +0 -1
  244. package/dist/cjs/orez.test.cjs +0 -211
  245. package/dist/cjs/orez.test.native.js +0 -226
  246. package/dist/cjs/orez.test.native.js.map +0 -1
  247. package/dist/esm/orez.mjs +0 -202
  248. package/dist/esm/orez.mjs.map +0 -1
  249. package/dist/esm/orez.native.js +0 -286
  250. package/dist/esm/orez.native.js.map +0 -1
  251. package/dist/esm/orez.test.mjs +0 -188
  252. package/dist/esm/orez.test.mjs.map +0 -1
  253. package/dist/esm/orez.test.native.js +0 -200
  254. package/dist/esm/orez.test.native.js.map +0 -1
  255. package/src/orez.test.ts +0 -195
  256. package/src/orez.ts +0 -331
  257. package/types/orez.d.ts +0 -53
  258. package/types/orez.d.ts.map +0 -1
  259. package/types/orez.test.d.ts +0 -2
  260. 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
+ }