on-zero 0.4.26 → 0.4.28
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 +14 -1
- package/dist/cjs/createZeroClient.native.js +15 -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 +588 -0
- package/dist/cjs/httpPull/transport.test.native.js +677 -0
- package/dist/cjs/httpPull/transport.test.native.js.map +1 -0
- package/dist/cjs/httpPullTransport.cjs +447 -0
- package/dist/cjs/httpPullTransport.native.js +710 -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 +14 -1
- package/dist/esm/createZeroClient.mjs.map +1 -1
- package/dist/esm/createZeroClient.native.js +15 -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 +589 -0
- package/dist/esm/httpPull/transport.test.mjs.map +1 -0
- package/dist/esm/httpPull/transport.test.native.js +675 -0
- package/dist/esm/httpPull/transport.test.native.js.map +1 -0
- package/dist/esm/httpPullTransport.mjs +421 -0
- package/dist/esm/httpPullTransport.mjs.map +1 -0
- package/dist/esm/httpPullTransport.native.js +681 -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 +22 -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 +603 -0
- package/src/httpPullTransport.ts +587 -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
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
import { createServer, type IncomingMessage, type ServerResponse } from 'node:http'
|
|
2
|
+
|
|
3
|
+
import type { AddressInfo } from 'node:net'
|
|
4
|
+
|
|
5
|
+
export type Row = Record<string, string>
|
|
6
|
+
|
|
7
|
+
type TableName = 'user' | 'project' | 'member'
|
|
8
|
+
type Tables = Record<TableName, Map<string, Row>>
|
|
9
|
+
type ClientMutationResults = Map<string, Map<string, Map<number, MutationResult>>>
|
|
10
|
+
|
|
11
|
+
interface PullBody {
|
|
12
|
+
clientID: string
|
|
13
|
+
clientGroupID: string
|
|
14
|
+
cookie: number | null
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface PushMutation {
|
|
18
|
+
type: string
|
|
19
|
+
name: string
|
|
20
|
+
clientID: string
|
|
21
|
+
id: number
|
|
22
|
+
args: Row[]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface PushBody {
|
|
26
|
+
clientGroupID: string
|
|
27
|
+
mutations: PushMutation[]
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
type MutationResult = Record<string, never> | { error: 'app'; details: string }
|
|
31
|
+
|
|
32
|
+
const tableNames: TableName[] = ['user', 'project', 'member']
|
|
33
|
+
|
|
34
|
+
export async function startZeroHttpServer(opts?: {
|
|
35
|
+
seed?: { user?: Row[]; project?: Row[]; member?: Row[] }
|
|
36
|
+
}): Promise<{
|
|
37
|
+
url: string
|
|
38
|
+
version(): number
|
|
39
|
+
rows(table: string): Row[]
|
|
40
|
+
close(): Promise<void>
|
|
41
|
+
}> {
|
|
42
|
+
const tables = seedTables(opts?.seed)
|
|
43
|
+
const lmids = new Map<string, Map<string, number>>()
|
|
44
|
+
const mutationResults: ClientMutationResults = new Map()
|
|
45
|
+
const clientGroupUsers = new Map<string, string>()
|
|
46
|
+
let cookie = 1
|
|
47
|
+
|
|
48
|
+
const server = createServer(async (req, res) => {
|
|
49
|
+
try {
|
|
50
|
+
if (req.method !== 'POST') {
|
|
51
|
+
sendJSON(res, 404, { error: 'not found' })
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const path = new URL(req.url || '/', 'http://127.0.0.1').pathname
|
|
56
|
+
const userID = authenticate(req, tables)
|
|
57
|
+
if (!userID) {
|
|
58
|
+
sendJSON(res, 401, { error: 'unauthorized' })
|
|
59
|
+
return
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (path === '/pull') {
|
|
63
|
+
const body = (await readJSON(req)) as PullBody
|
|
64
|
+
if (!bindClientGroup(clientGroupUsers, body.clientGroupID, userID)) {
|
|
65
|
+
sendJSON(res, 403, { error: 'client group belongs to a different user' })
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
if (body.cookie === cookie) {
|
|
69
|
+
sendJSON(res, 200, { cookie, unchanged: true })
|
|
70
|
+
return
|
|
71
|
+
}
|
|
72
|
+
if (typeof body.cookie === 'number' && body.cookie > cookie) {
|
|
73
|
+
sendJSON(res, 409, {
|
|
74
|
+
error: `future cookie ${body.cookie} is ahead of server cookie ${cookie}`,
|
|
75
|
+
})
|
|
76
|
+
return
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
sendJSON(res, 200, {
|
|
80
|
+
cookie,
|
|
81
|
+
lastMutationIDChanges: lastMutationIDChanges(lmids, body.clientGroupID),
|
|
82
|
+
rowsPatch: [{ op: 'clear' }, ...visibleRowsPatch(tables, userID)],
|
|
83
|
+
})
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (path === '/push') {
|
|
88
|
+
const body = (await readJSON(req)) as PushBody
|
|
89
|
+
if (!bindClientGroup(clientGroupUsers, body.clientGroupID, userID)) {
|
|
90
|
+
sendJSON(res, 403, { error: 'client group belongs to a different user' })
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
const mutations = Array.isArray(body.mutations) ? body.mutations : []
|
|
94
|
+
const gap = findMutationGap(lmids, body.clientGroupID, mutations)
|
|
95
|
+
if (gap) {
|
|
96
|
+
sendJSON(res, 500, { error: gap })
|
|
97
|
+
return
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const pushResults: Array<{
|
|
101
|
+
id: { clientID: string; id: number }
|
|
102
|
+
result: MutationResult
|
|
103
|
+
}> = []
|
|
104
|
+
let processedNewMutation = false
|
|
105
|
+
|
|
106
|
+
for (const mutation of mutations) {
|
|
107
|
+
const current = lmidFor(lmids, body.clientGroupID, mutation.clientID)
|
|
108
|
+
if (mutation.id <= current) {
|
|
109
|
+
pushResults.push({
|
|
110
|
+
id: { clientID: mutation.clientID, id: mutation.id },
|
|
111
|
+
result:
|
|
112
|
+
resultForMutation(
|
|
113
|
+
mutationResults,
|
|
114
|
+
body.clientGroupID,
|
|
115
|
+
mutation.clientID,
|
|
116
|
+
mutation.id,
|
|
117
|
+
) || {},
|
|
118
|
+
})
|
|
119
|
+
continue
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const result = applyMutation(tables, userID, mutation)
|
|
123
|
+
setLMID(lmids, body.clientGroupID, mutation.clientID, mutation.id)
|
|
124
|
+
setMutationResult(
|
|
125
|
+
mutationResults,
|
|
126
|
+
body.clientGroupID,
|
|
127
|
+
mutation.clientID,
|
|
128
|
+
mutation.id,
|
|
129
|
+
result,
|
|
130
|
+
)
|
|
131
|
+
processedNewMutation = true
|
|
132
|
+
pushResults.push({
|
|
133
|
+
id: { clientID: mutation.clientID, id: mutation.id },
|
|
134
|
+
result,
|
|
135
|
+
})
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (processedNewMutation) cookie += 1
|
|
139
|
+
sendJSON(res, 200, { pushResponse: { mutations: pushResults } })
|
|
140
|
+
return
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
sendJSON(res, 404, { error: 'not found' })
|
|
144
|
+
} catch (err) {
|
|
145
|
+
sendJSON(res, 500, { error: err instanceof Error ? err.message : String(err) })
|
|
146
|
+
}
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
await new Promise<void>((resolve, reject) => {
|
|
150
|
+
server.once('error', reject)
|
|
151
|
+
server.listen(0, '127.0.0.1', () => {
|
|
152
|
+
server.off('error', reject)
|
|
153
|
+
resolve()
|
|
154
|
+
})
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
const address = server.address() as AddressInfo
|
|
158
|
+
return {
|
|
159
|
+
url: `http://127.0.0.1:${address.port}`,
|
|
160
|
+
version: () => cookie,
|
|
161
|
+
rows: (table) => rowsForTable(tables, table),
|
|
162
|
+
close: () =>
|
|
163
|
+
new Promise((resolve, reject) => {
|
|
164
|
+
server.close((err) => (err ? reject(err) : resolve()))
|
|
165
|
+
}),
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function seedTables(seed?: { user?: Row[]; project?: Row[]; member?: Row[] }) {
|
|
170
|
+
const tables: Tables = {
|
|
171
|
+
user: new Map(),
|
|
172
|
+
project: new Map(),
|
|
173
|
+
member: new Map(),
|
|
174
|
+
}
|
|
175
|
+
for (const table of tableNames) {
|
|
176
|
+
for (const row of seed?.[table] || []) {
|
|
177
|
+
if (typeof row.id === 'string') tables[table].set(row.id, cloneRow(row))
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return tables
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function authenticate(req: IncomingMessage, tables: Tables) {
|
|
184
|
+
const header = req.headers.authorization
|
|
185
|
+
if (!header?.startsWith('Bearer token-')) return null
|
|
186
|
+
const userID = header.slice('Bearer token-'.length)
|
|
187
|
+
return tables.user.has(userID) ? userID : null
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function bindClientGroup(
|
|
191
|
+
clientGroupUsers: Map<string, string>,
|
|
192
|
+
clientGroupID: string,
|
|
193
|
+
userID: string,
|
|
194
|
+
) {
|
|
195
|
+
const owner = clientGroupUsers.get(clientGroupID)
|
|
196
|
+
if (owner) return owner === userID
|
|
197
|
+
clientGroupUsers.set(clientGroupID, userID)
|
|
198
|
+
return true
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function visibleRowsPatch(tables: Tables, userID: string) {
|
|
202
|
+
const visibleProjectIDs = visibleProjectIDSet(tables, userID)
|
|
203
|
+
const rows: Array<{ op: 'put'; tableName: TableName; value: Row }> = []
|
|
204
|
+
const user = tables.user.get(userID)
|
|
205
|
+
if (user) rows.push({ op: 'put', tableName: 'user', value: cloneRow(user) })
|
|
206
|
+
|
|
207
|
+
for (const project of tables.project.values()) {
|
|
208
|
+
if (visibleProjectIDs.has(project.id)) {
|
|
209
|
+
rows.push({ op: 'put', tableName: 'project', value: cloneRow(project) })
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
for (const member of tables.member.values()) {
|
|
214
|
+
if (visibleProjectIDs.has(member.projectId)) {
|
|
215
|
+
rows.push({ op: 'put', tableName: 'member', value: cloneRow(member) })
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return rows
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function visibleProjectIDSet(tables: Tables, userID: string) {
|
|
222
|
+
const projectIDs = new Set<string>()
|
|
223
|
+
for (const project of tables.project.values()) {
|
|
224
|
+
if (project.ownerId === userID) projectIDs.add(project.id)
|
|
225
|
+
}
|
|
226
|
+
for (const member of tables.member.values()) {
|
|
227
|
+
if (member.userId === userID && tables.project.has(member.projectId)) {
|
|
228
|
+
projectIDs.add(member.projectId)
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return projectIDs
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function findMutationGap(
|
|
235
|
+
lmids: Map<string, Map<string, number>>,
|
|
236
|
+
clientGroupID: string,
|
|
237
|
+
mutations: PushMutation[],
|
|
238
|
+
) {
|
|
239
|
+
const nextLMIDs = new Map<string, number>()
|
|
240
|
+
for (const mutation of mutations) {
|
|
241
|
+
const current =
|
|
242
|
+
nextLMIDs.get(mutation.clientID) ?? lmidFor(lmids, clientGroupID, mutation.clientID)
|
|
243
|
+
if (mutation.id <= current) continue
|
|
244
|
+
if (mutation.id !== current + 1) {
|
|
245
|
+
return `mutation id gap for ${mutation.clientID}: got ${mutation.id}, expected ${
|
|
246
|
+
current + 1
|
|
247
|
+
}`
|
|
248
|
+
}
|
|
249
|
+
nextLMIDs.set(mutation.clientID, mutation.id)
|
|
250
|
+
}
|
|
251
|
+
return null
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function applyMutation(
|
|
255
|
+
tables: Tables,
|
|
256
|
+
userID: string,
|
|
257
|
+
mutation: PushMutation,
|
|
258
|
+
): MutationResult {
|
|
259
|
+
if (mutation.type !== 'custom') return appError('unsupported')
|
|
260
|
+
const args = mutation.args[0] || {}
|
|
261
|
+
|
|
262
|
+
if (mutation.name === 'project|create') {
|
|
263
|
+
if (tables.project.has(args.id)) return appError('exists')
|
|
264
|
+
if (args.ownerId !== userID) return appError('forbidden')
|
|
265
|
+
tables.project.set(args.id, {
|
|
266
|
+
id: args.id,
|
|
267
|
+
ownerId: args.ownerId,
|
|
268
|
+
name: args.name,
|
|
269
|
+
})
|
|
270
|
+
return {}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (mutation.name === 'project|rename') {
|
|
274
|
+
const project = tables.project.get(args.id)
|
|
275
|
+
if (!project) return appError('not-found')
|
|
276
|
+
if (project.ownerId !== userID) return appError('forbidden')
|
|
277
|
+
tables.project.set(args.id, { ...project, name: args.name })
|
|
278
|
+
return {}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (mutation.name === 'member|add') {
|
|
282
|
+
const project = tables.project.get(args.projectId)
|
|
283
|
+
if (!project) return appError('not-found')
|
|
284
|
+
if (project.ownerId !== userID) return appError('forbidden')
|
|
285
|
+
if (tables.member.has(args.id)) return appError('exists')
|
|
286
|
+
tables.member.set(args.id, {
|
|
287
|
+
id: args.id,
|
|
288
|
+
projectId: args.projectId,
|
|
289
|
+
userId: args.userId,
|
|
290
|
+
})
|
|
291
|
+
return {}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (mutation.name === 'member|remove') {
|
|
295
|
+
const member = tables.member.get(args.id)
|
|
296
|
+
if (!member) return appError('not-found')
|
|
297
|
+
const project = tables.project.get(member.projectId)
|
|
298
|
+
if (!project) return appError('not-found')
|
|
299
|
+
if (project.ownerId !== userID) return appError('forbidden')
|
|
300
|
+
tables.member.delete(args.id)
|
|
301
|
+
return {}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return appError('unsupported')
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function appError(details: string): MutationResult {
|
|
308
|
+
return { error: 'app', details }
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function lastMutationIDChanges(
|
|
312
|
+
lmids: Map<string, Map<string, number>>,
|
|
313
|
+
clientGroupID: string,
|
|
314
|
+
) {
|
|
315
|
+
return Object.fromEntries(lmids.get(clientGroupID) || [])
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function lmidFor(
|
|
319
|
+
lmids: Map<string, Map<string, number>>,
|
|
320
|
+
clientGroupID: string,
|
|
321
|
+
clientID: string,
|
|
322
|
+
) {
|
|
323
|
+
return lmids.get(clientGroupID)?.get(clientID) || 0
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function setLMID(
|
|
327
|
+
lmids: Map<string, Map<string, number>>,
|
|
328
|
+
clientGroupID: string,
|
|
329
|
+
clientID: string,
|
|
330
|
+
id: number,
|
|
331
|
+
) {
|
|
332
|
+
let group = lmids.get(clientGroupID)
|
|
333
|
+
if (!group) {
|
|
334
|
+
group = new Map()
|
|
335
|
+
lmids.set(clientGroupID, group)
|
|
336
|
+
}
|
|
337
|
+
group.set(clientID, id)
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function resultForMutation(
|
|
341
|
+
results: ClientMutationResults,
|
|
342
|
+
clientGroupID: string,
|
|
343
|
+
clientID: string,
|
|
344
|
+
id: number,
|
|
345
|
+
) {
|
|
346
|
+
return results.get(clientGroupID)?.get(clientID)?.get(id)
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function setMutationResult(
|
|
350
|
+
results: ClientMutationResults,
|
|
351
|
+
clientGroupID: string,
|
|
352
|
+
clientID: string,
|
|
353
|
+
id: number,
|
|
354
|
+
result: MutationResult,
|
|
355
|
+
) {
|
|
356
|
+
let group = results.get(clientGroupID)
|
|
357
|
+
if (!group) {
|
|
358
|
+
group = new Map()
|
|
359
|
+
results.set(clientGroupID, group)
|
|
360
|
+
}
|
|
361
|
+
let client = group.get(clientID)
|
|
362
|
+
if (!client) {
|
|
363
|
+
client = new Map()
|
|
364
|
+
group.set(clientID, client)
|
|
365
|
+
}
|
|
366
|
+
client.set(id, result)
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function rowsForTable(tables: Tables, table: string): Row[] {
|
|
370
|
+
if (!isTableName(table)) return []
|
|
371
|
+
return [...tables[table].values()].map(cloneRow)
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function isTableName(table: string): table is TableName {
|
|
375
|
+
return (tableNames as string[]).includes(table)
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function cloneRow(row: Row): Row {
|
|
379
|
+
return { ...row }
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
async function readJSON(req: IncomingMessage): Promise<unknown> {
|
|
383
|
+
let body = ''
|
|
384
|
+
for await (const chunk of req) body += chunk
|
|
385
|
+
return body ? JSON.parse(body) : {}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function sendJSON(res: ServerResponse, status: number, body: unknown) {
|
|
389
|
+
res.writeHead(status, { 'content-type': 'application/json' })
|
|
390
|
+
res.end(JSON.stringify(body))
|
|
391
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
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('e2e smoke: stock zero client syncs over http against the fixture server', async () => {
|
|
18
|
+
harness = await startZeroHttpHarness({
|
|
19
|
+
seed: {
|
|
20
|
+
user: [{ id: 'u1', name: 'ada' }],
|
|
21
|
+
project: [{ id: 'p1', ownerId: 'u1', name: 'first' }],
|
|
22
|
+
member: [{ id: 'm1', projectId: 'p1', userId: 'u1' }],
|
|
23
|
+
},
|
|
24
|
+
})
|
|
25
|
+
const zero = harness.createZero('u1')
|
|
26
|
+
|
|
27
|
+
const view = zero.query.project.related('members').materialize()
|
|
28
|
+
const initial = await waitForComplete<any[]>(view)
|
|
29
|
+
expect(initial).toEqual([
|
|
30
|
+
{
|
|
31
|
+
id: 'p1',
|
|
32
|
+
ownerId: 'u1',
|
|
33
|
+
name: 'first',
|
|
34
|
+
members: [{ id: 'm1', projectId: 'p1', userId: 'u1' }],
|
|
35
|
+
},
|
|
36
|
+
])
|
|
37
|
+
|
|
38
|
+
const mutation = zero.mutate.project.create({
|
|
39
|
+
id: 'p2',
|
|
40
|
+
ownerId: 'u1',
|
|
41
|
+
name: 'second',
|
|
42
|
+
})
|
|
43
|
+
await mutation.client
|
|
44
|
+
await mutation.server
|
|
45
|
+
|
|
46
|
+
await eventually(() => {
|
|
47
|
+
const names = view.data.map((project: any) => project.name).sort()
|
|
48
|
+
expect(names).toEqual(['first', 'second'])
|
|
49
|
+
})
|
|
50
|
+
expect(
|
|
51
|
+
harness.server
|
|
52
|
+
.rows('project')
|
|
53
|
+
.map((row) => row.id)
|
|
54
|
+
.sort(),
|
|
55
|
+
).toEqual(['p1', 'p2'])
|
|
56
|
+
view.destroy()
|
|
57
|
+
})
|