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,296 @@
|
|
|
1
|
+
import {
|
|
2
|
+
PushProcessor,
|
|
3
|
+
handleQueryRequest as zeroHandleQueryRequest,
|
|
4
|
+
} from '@rocicorp/zero/server'
|
|
5
|
+
|
|
6
|
+
import { runAsyncTaskEntries } from './asyncTasks'
|
|
7
|
+
import {
|
|
8
|
+
createAsyncTaskCollector,
|
|
9
|
+
createZeroDirectServerRuntime,
|
|
10
|
+
resolveServerQuery,
|
|
11
|
+
type TransactionAttemptLifecycle,
|
|
12
|
+
type ZeroServerCommonOptions,
|
|
13
|
+
type ZeroServerDatabase,
|
|
14
|
+
} from './createZeroDirectServer'
|
|
15
|
+
import { createMutators } from './helpers/createMutators'
|
|
16
|
+
import { runWithAuthScope } from './helpers/mutatorContext'
|
|
17
|
+
import { runWithQueryContext } from './helpers/queryContext'
|
|
18
|
+
|
|
19
|
+
import type { AuthData, GenericModels } from './types'
|
|
20
|
+
import type { Schema as ZeroSchema } from '@rocicorp/zero'
|
|
21
|
+
|
|
22
|
+
type AlreadyProcessedMutation = {
|
|
23
|
+
id: {
|
|
24
|
+
clientID: string
|
|
25
|
+
id: number
|
|
26
|
+
}
|
|
27
|
+
result: {
|
|
28
|
+
error: 'alreadyProcessed'
|
|
29
|
+
details?: unknown
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
type PushRequestBody = {
|
|
34
|
+
clientGroupID?: unknown
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type BackgroundTaskScheduler = (promise: Promise<void>) => void
|
|
38
|
+
|
|
39
|
+
export type HandleMutationRequestOptions = {
|
|
40
|
+
authData: AuthData | null
|
|
41
|
+
request: Request
|
|
42
|
+
scheduleBackgroundTask?: BackgroundTaskScheduler
|
|
43
|
+
skipAsyncTasks?: boolean
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type PersistAlreadyProcessedArgs = {
|
|
47
|
+
clientGroupID: string
|
|
48
|
+
mutations: AlreadyProcessedMutation[]
|
|
49
|
+
upstreamSchema: string
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
type ZeroServerCoreOptions<
|
|
53
|
+
Schema extends ZeroSchema,
|
|
54
|
+
Models extends GenericModels,
|
|
55
|
+
ServerActions extends Record<string, unknown>,
|
|
56
|
+
> = ZeroServerCommonOptions<Schema, Models, ServerActions> & {
|
|
57
|
+
createDatabase: () => ZeroServerDatabase
|
|
58
|
+
persistAlreadyProcessed?: (args: PersistAlreadyProcessedArgs) => Promise<void>
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function createZeroServerCore<
|
|
62
|
+
Schema extends ZeroSchema,
|
|
63
|
+
Models extends GenericModels,
|
|
64
|
+
ServerActions extends Record<string, unknown>,
|
|
65
|
+
>({
|
|
66
|
+
createDatabase,
|
|
67
|
+
createServerActions,
|
|
68
|
+
defaultAllowAdminRole = 'all',
|
|
69
|
+
defaultMutateAuthData,
|
|
70
|
+
models,
|
|
71
|
+
mutations: mutationValidators,
|
|
72
|
+
persistAlreadyProcessed,
|
|
73
|
+
queries,
|
|
74
|
+
schema,
|
|
75
|
+
validateMutation,
|
|
76
|
+
validateQuery,
|
|
77
|
+
}: ZeroServerCoreOptions<Schema, Models, ServerActions>) {
|
|
78
|
+
const { permissions, server } = createZeroDirectServerRuntime({
|
|
79
|
+
createDatabase,
|
|
80
|
+
createServerActions,
|
|
81
|
+
defaultAllowAdminRole,
|
|
82
|
+
defaultMutateAuthData,
|
|
83
|
+
models,
|
|
84
|
+
mutations: mutationValidators,
|
|
85
|
+
queries,
|
|
86
|
+
schema,
|
|
87
|
+
validateMutation,
|
|
88
|
+
validateQuery,
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
const handleMutationRequest = async ({
|
|
92
|
+
authData,
|
|
93
|
+
request,
|
|
94
|
+
scheduleBackgroundTask,
|
|
95
|
+
skipAsyncTasks,
|
|
96
|
+
}: HandleMutationRequestOptions) => {
|
|
97
|
+
const tasks = createAsyncTaskCollector()
|
|
98
|
+
const recoveryRequest = request.clone()
|
|
99
|
+
const mutators = createMutators({
|
|
100
|
+
enqueueTask: tasks.enqueueTask,
|
|
101
|
+
can: permissions.can,
|
|
102
|
+
createServerActions,
|
|
103
|
+
environment: 'server',
|
|
104
|
+
models,
|
|
105
|
+
authData,
|
|
106
|
+
validateMutation,
|
|
107
|
+
mutationValidators,
|
|
108
|
+
})
|
|
109
|
+
const processor = new PushProcessor(
|
|
110
|
+
withTransactionLifecycle(createDatabase(), tasks.lifecycle),
|
|
111
|
+
)
|
|
112
|
+
const response = await processor.process(mutators, request)
|
|
113
|
+
|
|
114
|
+
if (persistAlreadyProcessed) {
|
|
115
|
+
const parsed = await parseAlreadyProcessed(recoveryRequest, response)
|
|
116
|
+
if (parsed) await persistAlreadyProcessed(parsed)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (!skipAsyncTasks && tasks.committed.length) {
|
|
120
|
+
const entries = tasks.committed.map((entry) => ({
|
|
121
|
+
...entry,
|
|
122
|
+
task: () => runWithAuthScope(authData, entry.task),
|
|
123
|
+
}))
|
|
124
|
+
await runAsyncTaskEntries(entries, { background: scheduleBackgroundTask })
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
response,
|
|
129
|
+
asyncTasks: tasks.committed,
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const handleQueryRequest = async ({
|
|
134
|
+
authData,
|
|
135
|
+
request,
|
|
136
|
+
}: {
|
|
137
|
+
authData: AuthData | null
|
|
138
|
+
request: Request
|
|
139
|
+
}) => {
|
|
140
|
+
if (!queries) {
|
|
141
|
+
throw new Error(
|
|
142
|
+
'No queries registered with createZeroServer. ' +
|
|
143
|
+
'Pass the syncedQueries registry to createZeroServer via the queries option.',
|
|
144
|
+
)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const handler = (name: string, args: unknown) =>
|
|
148
|
+
resolveServerQuery({
|
|
149
|
+
authData,
|
|
150
|
+
name,
|
|
151
|
+
args,
|
|
152
|
+
queries,
|
|
153
|
+
permissions,
|
|
154
|
+
validateQuery,
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
const userID = typeof authData?.id === 'string' ? authData.id : undefined
|
|
158
|
+
const response = await runWithQueryContext(
|
|
159
|
+
{ authData: authData || ({} as AuthData) },
|
|
160
|
+
() =>
|
|
161
|
+
// the legacy overload omits userID when auth does not identify a user.
|
|
162
|
+
userID === undefined
|
|
163
|
+
? zeroHandleQueryRequest(handler, schema, request)
|
|
164
|
+
: zeroHandleQueryRequest({ handler, schema, request, userID }),
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
return { response }
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
handleMutationRequest,
|
|
172
|
+
handleQueryRequest,
|
|
173
|
+
...server,
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export type ZeroServer<
|
|
178
|
+
Schema extends ZeroSchema,
|
|
179
|
+
Models extends GenericModels,
|
|
180
|
+
ServerActions extends Record<string, unknown>,
|
|
181
|
+
> = ReturnType<typeof createZeroServerCore<Schema, Models, ServerActions>>
|
|
182
|
+
|
|
183
|
+
function withTransactionLifecycle(
|
|
184
|
+
database: ZeroServerDatabase,
|
|
185
|
+
lifecycle: TransactionAttemptLifecycle,
|
|
186
|
+
): ZeroServerDatabase {
|
|
187
|
+
return {
|
|
188
|
+
async transaction(callback, input) {
|
|
189
|
+
try {
|
|
190
|
+
const value = await database.transaction(async (tx, hooks) => {
|
|
191
|
+
lifecycle.beginAttempt()
|
|
192
|
+
try {
|
|
193
|
+
const result = await callback(tx, {
|
|
194
|
+
...hooks,
|
|
195
|
+
async updateClientMutationID() {
|
|
196
|
+
const result = await hooks.updateClientMutationID()
|
|
197
|
+
if (input && input.mutationID < result.lastMutationID) {
|
|
198
|
+
throw new ExpectedDuplicateMutation(
|
|
199
|
+
input.clientID,
|
|
200
|
+
input.mutationID,
|
|
201
|
+
result.lastMutationID,
|
|
202
|
+
)
|
|
203
|
+
}
|
|
204
|
+
return result
|
|
205
|
+
},
|
|
206
|
+
})
|
|
207
|
+
lifecycle.completeAttempt()
|
|
208
|
+
return result
|
|
209
|
+
} catch (error) {
|
|
210
|
+
lifecycle.rollbackAttempt()
|
|
211
|
+
throw error
|
|
212
|
+
}
|
|
213
|
+
}, input)
|
|
214
|
+
lifecycle.commitTransaction()
|
|
215
|
+
return value
|
|
216
|
+
} catch (error) {
|
|
217
|
+
lifecycle.rollbackTransaction()
|
|
218
|
+
if (error instanceof ExpectedDuplicateMutation) {
|
|
219
|
+
return {
|
|
220
|
+
id: {
|
|
221
|
+
clientID: error.clientID,
|
|
222
|
+
id: error.mutationID,
|
|
223
|
+
},
|
|
224
|
+
result: {
|
|
225
|
+
error: 'alreadyProcessed',
|
|
226
|
+
details: error.message,
|
|
227
|
+
},
|
|
228
|
+
} as never
|
|
229
|
+
}
|
|
230
|
+
throw error
|
|
231
|
+
}
|
|
232
|
+
},
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
class ExpectedDuplicateMutation extends Error {
|
|
237
|
+
constructor(
|
|
238
|
+
readonly clientID: string,
|
|
239
|
+
readonly mutationID: number,
|
|
240
|
+
expectedMutationID: number | bigint,
|
|
241
|
+
) {
|
|
242
|
+
super(
|
|
243
|
+
`Ignoring mutation from ${clientID} with ID ${mutationID} as it was already processed. Expected: ${expectedMutationID}`,
|
|
244
|
+
)
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async function parseAlreadyProcessed(
|
|
249
|
+
request: Request,
|
|
250
|
+
response: unknown,
|
|
251
|
+
): Promise<PersistAlreadyProcessedArgs | undefined> {
|
|
252
|
+
const mutations = alreadyProcessedMutations(response)
|
|
253
|
+
if (mutations.length === 0) return
|
|
254
|
+
|
|
255
|
+
const upstreamSchema = new URL(request.url).searchParams.get('schema')
|
|
256
|
+
if (!upstreamSchema) return
|
|
257
|
+
|
|
258
|
+
const body = (await request.json()) as PushRequestBody
|
|
259
|
+
if (typeof body.clientGroupID !== 'string') return
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
clientGroupID: body.clientGroupID,
|
|
263
|
+
mutations,
|
|
264
|
+
upstreamSchema,
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function alreadyProcessedMutations(response: unknown): AlreadyProcessedMutation[] {
|
|
269
|
+
if (!response || typeof response !== 'object') return []
|
|
270
|
+
const mutations = (response as { mutations?: unknown }).mutations
|
|
271
|
+
if (!Array.isArray(mutations)) return []
|
|
272
|
+
|
|
273
|
+
return mutations.filter((mutation): mutation is AlreadyProcessedMutation => {
|
|
274
|
+
if (!mutation || typeof mutation !== 'object') return false
|
|
275
|
+
const id = (mutation as { id?: unknown }).id
|
|
276
|
+
const result = (mutation as { result?: unknown }).result
|
|
277
|
+
return (
|
|
278
|
+
!!id &&
|
|
279
|
+
typeof id === 'object' &&
|
|
280
|
+
typeof (id as { clientID?: unknown }).clientID === 'string' &&
|
|
281
|
+
typeof (id as { id?: unknown }).id === 'number' &&
|
|
282
|
+
!!result &&
|
|
283
|
+
typeof result === 'object' &&
|
|
284
|
+
(result as { error?: unknown }).error === 'alreadyProcessed'
|
|
285
|
+
)
|
|
286
|
+
})
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export type {
|
|
290
|
+
ValidateMutationArgs,
|
|
291
|
+
ValidateMutationFn,
|
|
292
|
+
ValidateQueryArgs,
|
|
293
|
+
ValidateQueryFn,
|
|
294
|
+
ZeroServerCommonOptions,
|
|
295
|
+
ZeroServerDatabase,
|
|
296
|
+
} from './createZeroDirectServer'
|
|
@@ -30,7 +30,6 @@ export function createMutators<Models extends GenericModels>({
|
|
|
30
30
|
models,
|
|
31
31
|
validateMutation,
|
|
32
32
|
mutationValidators,
|
|
33
|
-
resolveAuthData,
|
|
34
33
|
}: {
|
|
35
34
|
environment: 'server' | 'client'
|
|
36
35
|
authData: AuthData | null
|
|
@@ -41,7 +40,6 @@ export function createMutators<Models extends GenericModels>({
|
|
|
41
40
|
validateMutation?: ValidateMutationFn
|
|
42
41
|
/** valibot schemas keyed by model.mutationName, auto-validates args before running */
|
|
43
42
|
mutationValidators?: Record<string, Record<string, any>>
|
|
44
|
-
resolveAuthData?: () => AuthData | null
|
|
45
43
|
}): GetZeroMutators<Models> {
|
|
46
44
|
const serverActions = createServerActions?.()
|
|
47
45
|
|
|
@@ -56,8 +54,7 @@ export function createMutators<Models extends GenericModels>({
|
|
|
56
54
|
tx,
|
|
57
55
|
// on client, read authData dynamically to avoid stale closure during auth transitions
|
|
58
56
|
// (ZeroProvider recreates Zero instance in useEffect, but mutations can run before that)
|
|
59
|
-
authData:
|
|
60
|
-
environment === 'client' ? getAuthData() : (resolveAuthData?.() ?? authData),
|
|
57
|
+
authData: environment === 'client' ? getAuthData() : authData,
|
|
61
58
|
environment,
|
|
62
59
|
can,
|
|
63
60
|
server:
|
|
@@ -171,8 +168,7 @@ export function createMutators<Models extends GenericModels>({
|
|
|
171
168
|
// then run user-provided validation hook as escape hatch
|
|
172
169
|
if (validateMutation) {
|
|
173
170
|
await validateMutation({
|
|
174
|
-
authData:
|
|
175
|
-
environment === 'client' ? getAuthData() : (resolveAuthData?.() ?? authData),
|
|
171
|
+
authData: environment === 'client' ? getAuthData() : authData,
|
|
176
172
|
tableName,
|
|
177
173
|
mutatorName,
|
|
178
174
|
args: args[1],
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
eventually,
|
|
5
|
+
sleep,
|
|
6
|
+
startZeroHttpHarness,
|
|
7
|
+
waitForComplete,
|
|
8
|
+
type ZeroHttpHarness,
|
|
9
|
+
} from './testHarness'
|
|
10
|
+
|
|
11
|
+
let harness: ZeroHttpHarness | undefined
|
|
12
|
+
|
|
13
|
+
type UserRow = { id: string; name: string }
|
|
14
|
+
type MemberRow = { id: string; projectId: string; userId: string }
|
|
15
|
+
type ProjectWithMembers = {
|
|
16
|
+
id: string
|
|
17
|
+
ownerId: string
|
|
18
|
+
name: string
|
|
19
|
+
members: MemberRow[]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
afterEach(async () => {
|
|
23
|
+
await harness?.close()
|
|
24
|
+
harness = undefined
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
describe('zero-http auth parity', () => {
|
|
28
|
+
test('real clients only emit rows visible to their auth token', async () => {
|
|
29
|
+
harness = await startZeroHttpHarness({
|
|
30
|
+
seed: {
|
|
31
|
+
user: [
|
|
32
|
+
{ id: 'u1', name: 'ada' },
|
|
33
|
+
{ id: 'u2', name: 'ben' },
|
|
34
|
+
],
|
|
35
|
+
project: [
|
|
36
|
+
{ id: 'p-u1', ownerId: 'u1', name: 'u1 private' },
|
|
37
|
+
{ id: 'p-u2', ownerId: 'u2', name: 'u2 private' },
|
|
38
|
+
{ id: 'p-shared', ownerId: 'u2', name: 'shared' },
|
|
39
|
+
],
|
|
40
|
+
member: [
|
|
41
|
+
{ id: 'm-u1-owner', projectId: 'p-u1', userId: 'u1' },
|
|
42
|
+
{ id: 'm-u2-owner', projectId: 'p-u2', userId: 'u2' },
|
|
43
|
+
{ id: 'm-shared-u1', projectId: 'p-shared', userId: 'u1' },
|
|
44
|
+
{ id: 'm-shared-u2', projectId: 'p-shared', userId: 'u2' },
|
|
45
|
+
],
|
|
46
|
+
},
|
|
47
|
+
})
|
|
48
|
+
const u1 = harness.createZero('u1')
|
|
49
|
+
const u2 = harness.createZero('u2')
|
|
50
|
+
|
|
51
|
+
const u1Users = u1.query.user.materialize()
|
|
52
|
+
const u1Projects = u1.query.project.related('members').materialize()
|
|
53
|
+
const u2Users = u2.query.user.materialize()
|
|
54
|
+
const u2Projects = u2.query.project.related('members').materialize()
|
|
55
|
+
|
|
56
|
+
const u1UserEmissions: UserRow[][] = []
|
|
57
|
+
const u1ProjectEmissions: ProjectWithMembers[][] = []
|
|
58
|
+
const u2UserEmissions: UserRow[][] = []
|
|
59
|
+
const u2ProjectEmissions: ProjectWithMembers[][] = []
|
|
60
|
+
|
|
61
|
+
const stops = [
|
|
62
|
+
captureRows(u1Users, u1UserEmissions, normalizeUsers),
|
|
63
|
+
captureRows(u1Projects, u1ProjectEmissions, normalizeProjects),
|
|
64
|
+
captureRows(u2Users, u2UserEmissions, normalizeUsers),
|
|
65
|
+
captureRows(u2Projects, u2ProjectEmissions, normalizeProjects),
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
const [u1UsersComplete, u1ProjectsComplete, u2UsersComplete, u2ProjectsComplete] =
|
|
70
|
+
await Promise.all([
|
|
71
|
+
waitForComplete<UserRow[]>(u1Users),
|
|
72
|
+
waitForComplete<ProjectWithMembers[]>(u1Projects),
|
|
73
|
+
waitForComplete<UserRow[]>(u2Users),
|
|
74
|
+
waitForComplete<ProjectWithMembers[]>(u2Projects),
|
|
75
|
+
])
|
|
76
|
+
|
|
77
|
+
expect(normalizeUsers(u1UsersComplete)).toEqual([{ id: 'u1', name: 'ada' }])
|
|
78
|
+
expect(normalizeProjects(u1ProjectsComplete)).toEqual([
|
|
79
|
+
{
|
|
80
|
+
id: 'p-shared',
|
|
81
|
+
ownerId: 'u2',
|
|
82
|
+
name: 'shared',
|
|
83
|
+
members: [
|
|
84
|
+
{ id: 'm-shared-u1', projectId: 'p-shared', userId: 'u1' },
|
|
85
|
+
{ id: 'm-shared-u2', projectId: 'p-shared', userId: 'u2' },
|
|
86
|
+
],
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
id: 'p-u1',
|
|
90
|
+
ownerId: 'u1',
|
|
91
|
+
name: 'u1 private',
|
|
92
|
+
members: [{ id: 'm-u1-owner', projectId: 'p-u1', userId: 'u1' }],
|
|
93
|
+
},
|
|
94
|
+
])
|
|
95
|
+
|
|
96
|
+
expect(normalizeUsers(u2UsersComplete)).toEqual([{ id: 'u2', name: 'ben' }])
|
|
97
|
+
expect(normalizeProjects(u2ProjectsComplete)).toEqual([
|
|
98
|
+
{
|
|
99
|
+
id: 'p-shared',
|
|
100
|
+
ownerId: 'u2',
|
|
101
|
+
name: 'shared',
|
|
102
|
+
members: [
|
|
103
|
+
{ id: 'm-shared-u1', projectId: 'p-shared', userId: 'u1' },
|
|
104
|
+
{ id: 'm-shared-u2', projectId: 'p-shared', userId: 'u2' },
|
|
105
|
+
],
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
id: 'p-u2',
|
|
109
|
+
ownerId: 'u2',
|
|
110
|
+
name: 'u2 private',
|
|
111
|
+
members: [{ id: 'm-u2-owner', projectId: 'p-u2', userId: 'u2' }],
|
|
112
|
+
},
|
|
113
|
+
])
|
|
114
|
+
|
|
115
|
+
assertNoUserEmission(u1UserEmissions, 'u2')
|
|
116
|
+
assertNoProjectEmission(u1ProjectEmissions, 'p-u2', 'm-u2-owner')
|
|
117
|
+
assertNoUserEmission(u2UserEmissions, 'u1')
|
|
118
|
+
assertNoProjectEmission(u2ProjectEmissions, 'p-u1', 'm-u1-owner')
|
|
119
|
+
|
|
120
|
+
await harness.transport.pull()
|
|
121
|
+
await sleep(50)
|
|
122
|
+
assertNoUserEmission(u1UserEmissions, 'u2')
|
|
123
|
+
assertNoProjectEmission(u1ProjectEmissions, 'p-u2', 'm-u2-owner')
|
|
124
|
+
assertNoUserEmission(u2UserEmissions, 'u1')
|
|
125
|
+
assertNoProjectEmission(u2ProjectEmissions, 'p-u1', 'm-u1-owner')
|
|
126
|
+
} finally {
|
|
127
|
+
for (const stop of stops) stop()
|
|
128
|
+
u1Users.destroy()
|
|
129
|
+
u1Projects.destroy()
|
|
130
|
+
u2Users.destroy()
|
|
131
|
+
u2Projects.destroy()
|
|
132
|
+
}
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
test('unknown token 401 reaches needs-auth without materializing data', async () => {
|
|
136
|
+
harness = await startZeroHttpHarness({
|
|
137
|
+
seed: {
|
|
138
|
+
user: [{ id: 'u1', name: 'ada' }],
|
|
139
|
+
project: [{ id: 'p-u1', ownerId: 'u1', name: 'u1 private' }],
|
|
140
|
+
member: [{ id: 'm-u1-owner', projectId: 'p-u1', userId: 'u1' }],
|
|
141
|
+
},
|
|
142
|
+
})
|
|
143
|
+
const unknown = harness.createZero('missing')
|
|
144
|
+
const projects = unknown.query.project.related('members').materialize()
|
|
145
|
+
const emissions: ProjectWithMembers[][] = []
|
|
146
|
+
const stop = captureRows(projects, emissions, normalizeProjects)
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
await eventually(() =>
|
|
150
|
+
expect(unknown.connection.state.current.name).toBe('needs-auth'),
|
|
151
|
+
)
|
|
152
|
+
await sleep(50)
|
|
153
|
+
|
|
154
|
+
expect(projects.data).toEqual([])
|
|
155
|
+
expect(emissions.every((emission) => emission.length === 0)).toBe(true)
|
|
156
|
+
} finally {
|
|
157
|
+
stop()
|
|
158
|
+
projects.destroy()
|
|
159
|
+
}
|
|
160
|
+
})
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
// listener data is Immutable<T>; normalize maps to fresh mutable rows
|
|
164
|
+
function captureRows<T>(
|
|
165
|
+
view: { addListener(listener: (data: any, resultType: string) => void): () => void },
|
|
166
|
+
emissions: T[],
|
|
167
|
+
normalize: (data: T) => T,
|
|
168
|
+
) {
|
|
169
|
+
return view.addListener((data) => {
|
|
170
|
+
emissions.push(normalize(data))
|
|
171
|
+
})
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function assertNoUserEmission(emissions: UserRow[][], privateUserID: string) {
|
|
175
|
+
for (const emission of emissions) {
|
|
176
|
+
expect(emission.map((row) => row.id)).not.toContain(privateUserID)
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function assertNoProjectEmission(
|
|
181
|
+
emissions: ProjectWithMembers[][],
|
|
182
|
+
privateProjectID: string,
|
|
183
|
+
privateMemberID: string,
|
|
184
|
+
) {
|
|
185
|
+
for (const emission of emissions) {
|
|
186
|
+
expect(emission.map((row) => row.id)).not.toContain(privateProjectID)
|
|
187
|
+
expect(
|
|
188
|
+
emission.flatMap((row) => row.members.map((member) => member.id)),
|
|
189
|
+
).not.toContain(privateMemberID)
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function normalizeUsers(users: UserRow[]) {
|
|
194
|
+
return clone(users).sort((a, b) => a.id.localeCompare(b.id))
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function normalizeProjects(projects: ProjectWithMembers[]) {
|
|
198
|
+
return clone(projects)
|
|
199
|
+
.map((project) => ({
|
|
200
|
+
...project,
|
|
201
|
+
members: [...project.members].sort((a, b) => a.id.localeCompare(b.id)),
|
|
202
|
+
}))
|
|
203
|
+
.sort((a, b) => a.id.localeCompare(b.id))
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function clone<T>(value: T): T {
|
|
207
|
+
return JSON.parse(JSON.stringify(value)) as T
|
|
208
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { afterEach, expect, test } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
eventually,
|
|
5
|
+
startZeroHttpHarness,
|
|
6
|
+
waitForComplete,
|
|
7
|
+
type ZeroHttpHarness,
|
|
8
|
+
} from './testHarness'
|
|
9
|
+
|
|
10
|
+
let harness: ZeroHttpHarness | undefined
|
|
11
|
+
|
|
12
|
+
afterEach(async () => {
|
|
13
|
+
await harness?.close()
|
|
14
|
+
harness = undefined
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
test('mutation burst converges while explicit pulls churn', async () => {
|
|
18
|
+
let clientGroupID = ''
|
|
19
|
+
const pullErrors: unknown[] = []
|
|
20
|
+
|
|
21
|
+
harness = await startZeroHttpHarness({
|
|
22
|
+
seed: {
|
|
23
|
+
user: [{ id: 'u1', name: 'ada' }],
|
|
24
|
+
project: [],
|
|
25
|
+
member: [],
|
|
26
|
+
},
|
|
27
|
+
interceptFetch: (next) => async (input, init) => {
|
|
28
|
+
const body = init?.body ? JSON.parse(String(init.body)) : undefined
|
|
29
|
+
if (body?.clientGroupID) clientGroupID = body.clientGroupID
|
|
30
|
+
return next(input, init)
|
|
31
|
+
},
|
|
32
|
+
})
|
|
33
|
+
const zero = harness.createZero('u1')
|
|
34
|
+
const view = zero.query.project.materialize()
|
|
35
|
+
await waitForComplete<any[]>(view)
|
|
36
|
+
|
|
37
|
+
const timer = setInterval(() => {
|
|
38
|
+
void harness?.transport.pull().catch((error) => pullErrors.push(error))
|
|
39
|
+
}, 2)
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const mutations = Array.from({ length: 15 }, (_, index) =>
|
|
43
|
+
zero.mutate.project.create({
|
|
44
|
+
id: `p${index + 1}`,
|
|
45
|
+
ownerId: 'u1',
|
|
46
|
+
name: `project ${index + 1}`,
|
|
47
|
+
}),
|
|
48
|
+
)
|
|
49
|
+
await Promise.all(mutations.map((mutation) => mutation.client))
|
|
50
|
+
await Promise.all(mutations.map((mutation) => mutation.server))
|
|
51
|
+
await harness.transport.pull()
|
|
52
|
+
|
|
53
|
+
await eventually(() => {
|
|
54
|
+
expect(projectIDs(view.data)).toEqual(
|
|
55
|
+
Array.from({ length: 15 }, (_, index) => `p${index + 1}`).sort(),
|
|
56
|
+
)
|
|
57
|
+
})
|
|
58
|
+
} finally {
|
|
59
|
+
clearInterval(timer)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
expect(pullErrors).toEqual([])
|
|
63
|
+
expect(zero.connection.state.current.name).toBe('connected')
|
|
64
|
+
expect(
|
|
65
|
+
harness.server
|
|
66
|
+
.rows('project')
|
|
67
|
+
.map((row) => row.id)
|
|
68
|
+
.sort(),
|
|
69
|
+
).toEqual(Array.from({ length: 15 }, (_, index) => `p${index + 1}`).sort())
|
|
70
|
+
|
|
71
|
+
const pull = await rawPull(harness, clientGroupID)
|
|
72
|
+
expect(Object.values(pull.lastMutationIDChanges)).toContain(15)
|
|
73
|
+
view.destroy()
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
test('two clients sharing a client group converge without cookie fights', async () => {
|
|
77
|
+
const clientGroups = new Set<string>()
|
|
78
|
+
const clientIDs = new Set<string>()
|
|
79
|
+
|
|
80
|
+
harness = await startZeroHttpHarness({
|
|
81
|
+
seed: {
|
|
82
|
+
user: [{ id: 'u1', name: 'ada' }],
|
|
83
|
+
project: [{ id: 'p1', ownerId: 'u1', name: 'first' }],
|
|
84
|
+
member: [],
|
|
85
|
+
},
|
|
86
|
+
interceptFetch: (next) => async (input, init) => {
|
|
87
|
+
const body = init?.body ? JSON.parse(String(init.body)) : undefined
|
|
88
|
+
if (body?.clientGroupID) clientGroups.add(body.clientGroupID)
|
|
89
|
+
if (body?.clientID) clientIDs.add(body.clientID)
|
|
90
|
+
return next(input, init)
|
|
91
|
+
},
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
const sharedStorageKey = 'zero-http-shared-client-group'
|
|
95
|
+
const zeroA = harness.createZero('u1', { storageKey: sharedStorageKey })
|
|
96
|
+
const zeroB = harness.createZero('u1', { storageKey: sharedStorageKey })
|
|
97
|
+
const viewA = zeroA.query.project.materialize()
|
|
98
|
+
const viewB = zeroB.query.project.materialize()
|
|
99
|
+
await waitForComplete<any[]>(viewA)
|
|
100
|
+
await waitForComplete<any[]>(viewB)
|
|
101
|
+
|
|
102
|
+
expect(harness.transport.connections).toBe(2)
|
|
103
|
+
expect(clientGroups.size).toBe(1)
|
|
104
|
+
expect(clientIDs.size).toBe(2)
|
|
105
|
+
|
|
106
|
+
const mutation = zeroA.mutate.project.create({
|
|
107
|
+
id: 'p2',
|
|
108
|
+
ownerId: 'u1',
|
|
109
|
+
name: 'second',
|
|
110
|
+
})
|
|
111
|
+
await mutation.client
|
|
112
|
+
await mutation.server
|
|
113
|
+
await harness.transport.pull()
|
|
114
|
+
|
|
115
|
+
await eventually(() => {
|
|
116
|
+
expect(projectIDs(viewA.data)).toEqual(['p1', 'p2'])
|
|
117
|
+
expect(projectIDs(viewB.data)).toEqual(['p1', 'p2'])
|
|
118
|
+
})
|
|
119
|
+
expect(zeroA.connection.state.current.name).toBe('connected')
|
|
120
|
+
expect(zeroB.connection.state.current.name).toBe('connected')
|
|
121
|
+
|
|
122
|
+
viewA.destroy()
|
|
123
|
+
viewB.destroy()
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
async function rawPull(harness: ZeroHttpHarness, clientGroupID: string) {
|
|
127
|
+
const response = await fetch(`${harness.server.url}/pull`, {
|
|
128
|
+
method: 'POST',
|
|
129
|
+
headers: {
|
|
130
|
+
authorization: 'Bearer token-u1',
|
|
131
|
+
'content-type': 'application/json',
|
|
132
|
+
},
|
|
133
|
+
body: JSON.stringify({
|
|
134
|
+
clientID: 'raw-pull',
|
|
135
|
+
clientGroupID,
|
|
136
|
+
cookie: null,
|
|
137
|
+
}),
|
|
138
|
+
})
|
|
139
|
+
expect(response.status).toBe(200)
|
|
140
|
+
return response.json() as Promise<{
|
|
141
|
+
lastMutationIDChanges: Record<string, number>
|
|
142
|
+
}>
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function projectIDs(rows: any[]) {
|
|
146
|
+
return rows.map((row) => row.id).sort()
|
|
147
|
+
}
|