on-zero 0.4.26 → 0.4.27
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/createZeroClient.cjs +13 -1
- package/dist/cjs/createZeroClient.native.js +13 -1
- package/dist/cjs/createZeroClient.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/fixtureSchema.cjs +76 -0
- package/dist/cjs/httpPull/fixtureSchema.native.js +82 -0
- package/dist/cjs/httpPull/fixtureSchema.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/httpPull/transport.test.cjs +568 -0
- package/dist/cjs/httpPull/transport.test.native.js +655 -0
- package/dist/cjs/httpPull/transport.test.native.js.map +1 -0
- package/dist/cjs/httpPullTransport.cjs +432 -0
- package/dist/cjs/httpPullTransport.native.js +695 -0
- package/dist/cjs/httpPullTransport.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/esm/createZeroClient.mjs +13 -1
- package/dist/esm/createZeroClient.mjs.map +1 -1
- package/dist/esm/createZeroClient.native.js +13 -1
- package/dist/esm/createZeroClient.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/fixtureSchema.mjs +50 -0
- package/dist/esm/httpPull/fixtureSchema.mjs.map +1 -0
- package/dist/esm/httpPull/fixtureSchema.native.js +53 -0
- package/dist/esm/httpPull/fixtureSchema.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/httpPull/transport.test.mjs +569 -0
- package/dist/esm/httpPull/transport.test.mjs.map +1 -0
- package/dist/esm/httpPull/transport.test.native.js +653 -0
- package/dist/esm/httpPull/transport.test.native.js.map +1 -0
- package/dist/esm/httpPullTransport.mjs +406 -0
- package/dist/esm/httpPullTransport.mjs.map +1 -0
- package/dist/esm/httpPullTransport.native.js +666 -0
- package/dist/esm/httpPullTransport.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/package.json +2 -2
- package/src/createZeroClient.tsx +19 -0
- package/src/httpPull/auth.test.ts +208 -0
- package/src/httpPull/churn.test.ts +147 -0
- package/src/httpPull/fixtureSchema.ts +82 -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/httpPull/transport.test.ts +577 -0
- package/src/httpPullTransport.ts +559 -0
- package/src/index.ts +1 -0
- package/types/createZeroClient.d.ts +3 -1
- package/types/createZeroClient.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/fixtureSchema.d.ts +111 -0
- package/types/httpPull/fixtureSchema.d.ts.map +1 -0
- 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/httpPull/transport.test.d.ts +2 -0
- package/types/httpPull/transport.test.d.ts.map +1 -0
- package/types/httpPullTransport.d.ts +13 -0
- package/types/httpPullTransport.d.ts.map +1 -0
- package/types/index.d.ts +1 -0
- package/types/index.d.ts.map +1 -1
package/dist/esm/index.js
CHANGED
|
@@ -9,6 +9,7 @@ import { ensureAuth, getAuth } from "./helpers/getAuth.mjs";
|
|
|
9
9
|
import { setAuthData, setEnvironment } from "./state.mjs";
|
|
10
10
|
export * from "./combineZeroClients.mjs";
|
|
11
11
|
export * from "./createZeroClient.mjs";
|
|
12
|
+
export * from "./httpPullTransport.mjs";
|
|
12
13
|
export * from "./createUseQuery.mjs";
|
|
13
14
|
export * from "./resolveQuery.mjs";
|
|
14
15
|
export * from "./run.mjs";
|
package/dist/esm/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["ensureAuth","getAuth","setAuthData","setEnvironment","setRunner","defineQuery","defineQueries","clearZeroClientData","showZeroClientErrorOnce","resetShownZeroClientError"],"sources":["../../src/index.ts"],"sourcesContent":[null],"mappings":"AAAA,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,SAASA,UAAA,EAAYC,OAAA,QAAe;AACpC,SAASC,WAAA,EAAaC,cAAA,QAAsB;AAE5C,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,SAASC,SAAA,QAAkC;AAC3C,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,SAASC,WAAA,EAAaC,aAAA,QAAqB;AAQ3C,SACEC,mBAAA,QAEK;AACP,SACEC,uBAAA,EACAC,yBAAA,QAGK","ignoreList":[]}
|
|
1
|
+
{"version":3,"names":["ensureAuth","getAuth","setAuthData","setEnvironment","setRunner","defineQuery","defineQueries","clearZeroClientData","showZeroClientErrorOnce","resetShownZeroClientError"],"sources":["../../src/index.ts"],"sourcesContent":[null],"mappings":"AAAA,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,SAASA,UAAA,EAAYC,OAAA,QAAe;AACpC,SAASC,WAAA,EAAaC,cAAA,QAAsB;AAE5C,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,SAASC,SAAA,QAAkC;AAC3C,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,SAASC,WAAA,EAAaC,aAAA,QAAqB;AAQ3C,SACEC,mBAAA,QAEK;AACP,SACEC,uBAAA,EACAC,yBAAA,QAGK","ignoreList":[]}
|
package/dist/esm/index.mjs
CHANGED
|
@@ -9,6 +9,7 @@ import { ensureAuth, getAuth } from "./helpers/getAuth.mjs";
|
|
|
9
9
|
import { setAuthData, setEnvironment } from "./state.mjs";
|
|
10
10
|
export * from "./combineZeroClients.mjs";
|
|
11
11
|
export * from "./createZeroClient.mjs";
|
|
12
|
+
export * from "./httpPullTransport.mjs";
|
|
12
13
|
export * from "./createUseQuery.mjs";
|
|
13
14
|
export * from "./resolveQuery.mjs";
|
|
14
15
|
export * from "./run.mjs";
|
package/dist/esm/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["ensureAuth","getAuth","setAuthData","setEnvironment","setRunner","defineQuery","defineQueries","clearZeroClientData","showZeroClientErrorOnce","resetShownZeroClientError"],"sources":["../../src/index.ts"],"sourcesContent":[null],"mappings":"AAAA,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,SAASA,UAAA,EAAYC,OAAA,QAAe;AACpC,SAASC,WAAA,EAAaC,cAAA,QAAsB;AAE5C,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,SAASC,SAAA,QAAkC;AAC3C,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,SAASC,WAAA,EAAaC,aAAA,QAAqB;AAQ3C,SACEC,mBAAA,QAEK;AACP,SACEC,uBAAA,EACAC,yBAAA,QAGK","ignoreList":[]}
|
|
1
|
+
{"version":3,"names":["ensureAuth","getAuth","setAuthData","setEnvironment","setRunner","defineQuery","defineQueries","clearZeroClientData","showZeroClientErrorOnce","resetShownZeroClientError"],"sources":["../../src/index.ts"],"sourcesContent":[null],"mappings":"AAAA,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,SAASA,UAAA,EAAYC,OAAA,QAAe;AACpC,SAASC,WAAA,EAAaC,cAAA,QAAsB;AAE5C,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,SAASC,SAAA,QAAkC;AAC3C,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,SAASC,WAAA,EAAaC,aAAA,QAAqB;AAQ3C,SACEC,mBAAA,QAEK;AACP,SACEC,uBAAA,EACAC,yBAAA,QAGK","ignoreList":[]}
|
package/dist/esm/index.native.js
CHANGED
|
@@ -9,6 +9,7 @@ import { ensureAuth, getAuth } from "./helpers/getAuth.native.js";
|
|
|
9
9
|
import { setAuthData, setEnvironment } from "./state.native.js";
|
|
10
10
|
export * from "./combineZeroClients.native.js";
|
|
11
11
|
export * from "./createZeroClient.native.js";
|
|
12
|
+
export * from "./httpPullTransport.native.js";
|
|
12
13
|
export * from "./createUseQuery.native.js";
|
|
13
14
|
export * from "./resolveQuery.native.js";
|
|
14
15
|
export * from "./run.native.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["ensureAuth","getAuth","setAuthData","setEnvironment","setRunner","defineQuery","defineQueries","clearZeroClientData","showZeroClientErrorOnce","resetShownZeroClientError"],"sources":["../../src/index.ts"],"sourcesContent":[null],"mappings":"AAAA,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,SAASA,UAAA,EAAYC,OAAA,QAAe;AACpC,SAASC,WAAA,EAAaC,cAAA,QAAsB;AAE5C,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,SAASC,SAAA,QAAkC;AAC3C,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,SAASC,WAAA,EAAaC,aAAA,QAAqB;AAQ3C,SAAAC,mBAAA;AAAA,SACEC,uBAAA,EAAAC,yBAAA;AAAA,SAGFF,mBAAA,EACED,aAAA,EACAD,WAAA,EAAAL,UAGK,E","ignoreList":[]}
|
|
1
|
+
{"version":3,"names":["ensureAuth","getAuth","setAuthData","setEnvironment","setRunner","defineQuery","defineQueries","clearZeroClientData","showZeroClientErrorOnce","resetShownZeroClientError"],"sources":["../../src/index.ts"],"sourcesContent":[null],"mappings":"AAAA,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,SAASA,UAAA,EAAYC,OAAA,QAAe;AACpC,SAASC,WAAA,EAAaC,cAAA,QAAsB;AAE5C,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,SAASC,SAAA,QAAkC;AAC3C,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,SAASC,WAAA,EAAaC,aAAA,QAAqB;AAQ3C,SAAAC,mBAAA;AAAA,SACEC,uBAAA,EAAAC,yBAAA;AAAA,SAGFF,mBAAA,EACED,aAAA,EACAD,WAAA,EAAAL,UAGK,E","ignoreList":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "on-zero",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.27",
|
|
4
4
|
"description": "A typed layer over @rocicorp/zero with queries, mutations, and permissions",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"source": "src/index.ts",
|
|
@@ -77,7 +77,7 @@
|
|
|
77
77
|
}
|
|
78
78
|
},
|
|
79
79
|
"dependencies": {
|
|
80
|
-
"@take-out/helpers": "0.4.
|
|
80
|
+
"@take-out/helpers": "0.4.27",
|
|
81
81
|
"chokidar": "^4.0.3",
|
|
82
82
|
"citty": "^0.1.6",
|
|
83
83
|
"valibot": "^1.1.0"
|
package/src/createZeroClient.tsx
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
} from 'react'
|
|
14
14
|
|
|
15
15
|
import { createPermissions } from './createPermissions'
|
|
16
|
+
import { ensureHttpPullTransport } from './httpPullTransport'
|
|
16
17
|
import {
|
|
17
18
|
createUseQuery,
|
|
18
19
|
createUseQueryDirect,
|
|
@@ -331,11 +332,20 @@ export function createZeroClient<
|
|
|
331
332
|
children,
|
|
332
333
|
authData: authDataIn,
|
|
333
334
|
disable,
|
|
335
|
+
transport,
|
|
336
|
+
pullIntervalMs = 30_000,
|
|
334
337
|
...props
|
|
335
338
|
}: Omit<ZeroOptions<Schema, ZeroMutators>, 'schema' | 'mutators'> & {
|
|
336
339
|
children: ReactNode
|
|
337
340
|
authData?: AuthData | null
|
|
338
341
|
disable?: boolean
|
|
342
|
+
// 'http-pull' runs the stock zero client over stateless HTTP pull/push
|
|
343
|
+
// (no websocket, no resident server state) by intercepting the sync
|
|
344
|
+
// socket for this instance's server origin. see httpPullTransport.ts.
|
|
345
|
+
transport?: 'http-pull'
|
|
346
|
+
// http-pull only: every open connection also pulls on this interval so
|
|
347
|
+
// server-initiated changes arrive without a client-side trigger
|
|
348
|
+
pullIntervalMs?: number
|
|
339
349
|
}) => {
|
|
340
350
|
const authData = (authDataIn ?? null) as AuthData
|
|
341
351
|
|
|
@@ -374,6 +384,7 @@ export function createZeroClient<
|
|
|
374
384
|
.sort(([a], [b]) => (a < b ? -1 : 1)),
|
|
375
385
|
hasAuth,
|
|
376
386
|
generation,
|
|
387
|
+
transport,
|
|
377
388
|
])
|
|
378
389
|
|
|
379
390
|
// create/rotate in an effect — commit-safe: a discarded concurrent render
|
|
@@ -391,6 +402,14 @@ export function createZeroClient<
|
|
|
391
402
|
ZeroOptions<Schema, ZeroMutators>,
|
|
392
403
|
'schema' | 'mutators'
|
|
393
404
|
>
|
|
405
|
+
// install before construction so the instance's first connect goes
|
|
406
|
+
// through HTTP. per-origin idempotent — rotation reuses the install.
|
|
407
|
+
if (transport === 'http-pull') {
|
|
408
|
+
if (typeof options.server !== 'string') {
|
|
409
|
+
throw new Error(`transport 'http-pull' requires a server URL`)
|
|
410
|
+
}
|
|
411
|
+
ensureHttpPullTransport({ origin: options.server, pullIntervalMs })
|
|
412
|
+
}
|
|
394
413
|
cached = {
|
|
395
414
|
key: instanceKey,
|
|
396
415
|
instance: new ZeroClient<Schema, ZeroMutators>({
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { createSchema, relationships, string, table } from '@rocicorp/zero'
|
|
2
|
+
|
|
3
|
+
import type { Transaction } from '@rocicorp/zero'
|
|
4
|
+
|
|
5
|
+
const user = table('user')
|
|
6
|
+
.columns({
|
|
7
|
+
id: string(),
|
|
8
|
+
name: string(),
|
|
9
|
+
})
|
|
10
|
+
.primaryKey('id')
|
|
11
|
+
|
|
12
|
+
const project = table('project')
|
|
13
|
+
.columns({
|
|
14
|
+
id: string(),
|
|
15
|
+
ownerId: string(),
|
|
16
|
+
name: string(),
|
|
17
|
+
})
|
|
18
|
+
.primaryKey('id')
|
|
19
|
+
|
|
20
|
+
const member = table('member')
|
|
21
|
+
.columns({
|
|
22
|
+
id: string(),
|
|
23
|
+
projectId: string(),
|
|
24
|
+
userId: string(),
|
|
25
|
+
})
|
|
26
|
+
.primaryKey('id')
|
|
27
|
+
|
|
28
|
+
export const zeroHttpFixtureSchema = createSchema({
|
|
29
|
+
tables: [user, project, member],
|
|
30
|
+
relationships: [
|
|
31
|
+
relationships(project, ({ many }) => ({
|
|
32
|
+
members: many({
|
|
33
|
+
sourceField: ['id'],
|
|
34
|
+
destField: ['projectId'],
|
|
35
|
+
destSchema: member,
|
|
36
|
+
}),
|
|
37
|
+
})),
|
|
38
|
+
],
|
|
39
|
+
enableLegacyQueries: true,
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
type FixtureTransaction = Transaction<typeof zeroHttpFixtureSchema>
|
|
43
|
+
|
|
44
|
+
export type ProjectCreateArgs = {
|
|
45
|
+
id: string
|
|
46
|
+
ownerId: string
|
|
47
|
+
name: string
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export type ProjectRenameArgs = {
|
|
51
|
+
id: string
|
|
52
|
+
name: string
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export type MemberAddArgs = {
|
|
56
|
+
id: string
|
|
57
|
+
projectId: string
|
|
58
|
+
userId: string
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export type MemberRemoveArgs = {
|
|
62
|
+
id: string
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export const zeroHttpFixtureMutators = {
|
|
66
|
+
project: {
|
|
67
|
+
create: async (tx: FixtureTransaction, args: ProjectCreateArgs) => {
|
|
68
|
+
await tx.mutate.project.insert(args)
|
|
69
|
+
},
|
|
70
|
+
rename: async (tx: FixtureTransaction, args: ProjectRenameArgs) => {
|
|
71
|
+
await tx.mutate.project.update(args)
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
member: {
|
|
75
|
+
add: async (tx: FixtureTransaction, args: MemberAddArgs) => {
|
|
76
|
+
await tx.mutate.member.insert(args)
|
|
77
|
+
},
|
|
78
|
+
remove: async (tx: FixtureTransaction, args: MemberRemoveArgs) => {
|
|
79
|
+
await tx.mutate.member.delete({ id: args.id })
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
}
|