orez-sync-cf-host 0.5.16 → 0.5.18
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/README.md +66 -3
- package/package.json +1 -1
- package/src/config.ts +23 -0
- package/src/generated/sync_wasm.d.ts +35 -0
- package/src/generated/sync_wasm.js +97 -0
- package/src/generated/sync_wasm_bg.wasm +0 -0
- package/src/harness-config.ts +105 -1
- package/src/harness-worker.ts +22 -2
- package/src/host.ts +446 -137
- package/src/ingest-harness-worker.ts +75 -4
- package/src/types.ts +9 -1
- package/src/write-safeguards.ts +14 -15
|
@@ -32,15 +32,19 @@ const numericTextNamespaces = new Set<string>()
|
|
|
32
32
|
const jsonValueNamespaces = new Set<string>()
|
|
33
33
|
const hydratedNamespaces = new Set<string>()
|
|
34
34
|
const heldSnapshots = new Set<string>()
|
|
35
|
+
const holdSnapshotsAfterCursor = new Set<string>()
|
|
35
36
|
const activeSnapshots = new Set<string>()
|
|
37
|
+
const snapshotLimits = new Map<string, number[]>()
|
|
36
38
|
let delegatedFailuresRemaining = 0
|
|
37
39
|
let delegatedAttempts = 0
|
|
38
40
|
let delegatedPushFailedRemaining = 0
|
|
41
|
+
let delegatedUrl = ''
|
|
39
42
|
|
|
40
43
|
const config: SyncHostConfig<Env> = {
|
|
41
44
|
hostVersion: 'upstream-ingest-harness',
|
|
42
45
|
schema,
|
|
43
|
-
mutateUrl: '/api/zero/push',
|
|
46
|
+
mutateUrl: '/api/zero/push?schema=feed_0&appID=feed',
|
|
47
|
+
mutateOrigin: 'https://app.internal',
|
|
44
48
|
mutateBinding: 'APP',
|
|
45
49
|
delegatedPushRetry: {
|
|
46
50
|
maxAttempts: 3,
|
|
@@ -50,9 +54,11 @@ const config: SyncHostConfig<Env> = {
|
|
|
50
54
|
},
|
|
51
55
|
upstream: {
|
|
52
56
|
binding: 'DATA',
|
|
53
|
-
namespacePath: (namespace) =>
|
|
57
|
+
namespacePath: (namespace) =>
|
|
58
|
+
namespace.startsWith('root-mount-') ? '/' : `/${namespace}`,
|
|
54
59
|
changeLimit: 2,
|
|
55
60
|
intervalMs: 1_000,
|
|
61
|
+
ingestBudgetRows: 600,
|
|
56
62
|
},
|
|
57
63
|
initialize(sql) {
|
|
58
64
|
sql.exec(
|
|
@@ -63,6 +69,12 @@ const config: SyncHostConfig<Env> = {
|
|
|
63
69
|
const userID = request.headers.get('authorization')?.match(/^Bearer token-(.+)$/)?.[1]
|
|
64
70
|
return userID ? { userID } : null
|
|
65
71
|
},
|
|
72
|
+
authorizeWake(request) {
|
|
73
|
+
return new URL(request.url).searchParams.get('wakeToken') === 'ingest-harness-wake'
|
|
74
|
+
},
|
|
75
|
+
authorizeNotify(request, env) {
|
|
76
|
+
return Boolean(env.ADMIN_KEY) && request.headers.get('x-admin-key') === env.ADMIN_KEY
|
|
77
|
+
},
|
|
66
78
|
namespace(request) {
|
|
67
79
|
return new URL(request.url).pathname.split('/')[1] || null
|
|
68
80
|
},
|
|
@@ -113,6 +125,35 @@ export class DataService extends WorkerEntrypoint<Env> {
|
|
|
113
125
|
new Response('DATA route rejected non-feed request', { status: 418 })
|
|
114
126
|
)
|
|
115
127
|
}
|
|
128
|
+
if (pathname === '/changes') {
|
|
129
|
+
const cursor = Number(url.searchParams.get('watermark') ?? 0)
|
|
130
|
+
return Promise.resolve(
|
|
131
|
+
Response.json({
|
|
132
|
+
watermark: 1,
|
|
133
|
+
changes:
|
|
134
|
+
cursor >= 1
|
|
135
|
+
? []
|
|
136
|
+
: [
|
|
137
|
+
{
|
|
138
|
+
watermark: 1,
|
|
139
|
+
tableName: 'item',
|
|
140
|
+
op: 'INSERT',
|
|
141
|
+
rowData: {
|
|
142
|
+
id: 'root-feed-row',
|
|
143
|
+
label: 'root-mounted upstream feed',
|
|
144
|
+
rank: 1,
|
|
145
|
+
done: false,
|
|
146
|
+
meta: null,
|
|
147
|
+
},
|
|
148
|
+
oldData: null,
|
|
149
|
+
},
|
|
150
|
+
],
|
|
151
|
+
})
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
if (pathname === '/_orez/write-budget') {
|
|
155
|
+
return Promise.resolve(Response.json({ enabled: true, rootMount: true }))
|
|
156
|
+
}
|
|
116
157
|
const namespace = pathname.split('/')[1] ?? ''
|
|
117
158
|
if (pathname.endsWith('/changes') && jsonValueNamespaces.has(namespace)) {
|
|
118
159
|
const cursor = Number(new URL(request.url).searchParams.get('watermark') ?? 0)
|
|
@@ -211,10 +252,24 @@ export class DataService extends WorkerEntrypoint<Env> {
|
|
|
211
252
|
}
|
|
212
253
|
const response = await upstreamFetch(request, this.env)
|
|
213
254
|
if (pathname.endsWith('/snapshot')) {
|
|
214
|
-
|
|
255
|
+
const limit = Number(url.searchParams.get('limit'))
|
|
256
|
+
if (Number.isSafeInteger(limit)) {
|
|
257
|
+
const limits = snapshotLimits.get(namespace) ?? []
|
|
258
|
+
limits.push(limit)
|
|
259
|
+
snapshotLimits.set(namespace, limits)
|
|
260
|
+
}
|
|
261
|
+
const shouldHold =
|
|
262
|
+
heldSnapshots.has(namespace) &&
|
|
263
|
+
(!holdSnapshotsAfterCursor.has(namespace) || url.searchParams.has('cursor'))
|
|
264
|
+
if (shouldHold) {
|
|
215
265
|
activeSnapshots.add(namespace)
|
|
216
266
|
try {
|
|
217
|
-
while (
|
|
267
|
+
while (
|
|
268
|
+
heldSnapshots.has(namespace) &&
|
|
269
|
+
(!holdSnapshotsAfterCursor.has(namespace) || url.searchParams.has('cursor'))
|
|
270
|
+
) {
|
|
271
|
+
await scheduler.wait(10)
|
|
272
|
+
}
|
|
218
273
|
} finally {
|
|
219
274
|
activeSnapshots.delete(namespace)
|
|
220
275
|
}
|
|
@@ -227,6 +282,7 @@ export class DataService extends WorkerEntrypoint<Env> {
|
|
|
227
282
|
|
|
228
283
|
export class AppService extends WorkerEntrypoint<Env> {
|
|
229
284
|
fetch(request: Request): Promise<Response> {
|
|
285
|
+
delegatedUrl = request.url
|
|
230
286
|
if (!new URL(request.url).pathname.endsWith('/api/zero/push')) {
|
|
231
287
|
return Promise.resolve(
|
|
232
288
|
new Response('APP route rejected non-push request', { status: 418 })
|
|
@@ -272,6 +328,8 @@ export default {
|
|
|
272
328
|
Response.json({
|
|
273
329
|
active: activeSnapshots.has(namespace),
|
|
274
330
|
held: heldSnapshots.has(namespace),
|
|
331
|
+
afterCursor: holdSnapshotsAfterCursor.has(namespace),
|
|
332
|
+
limits: snapshotLimits.get(namespace) ?? [],
|
|
275
333
|
})
|
|
276
334
|
)
|
|
277
335
|
}
|
|
@@ -281,12 +339,22 @@ export default {
|
|
|
281
339
|
.then((body) => {
|
|
282
340
|
if ((body as { hold?: unknown }).hold === true) {
|
|
283
341
|
heldSnapshots.add(namespace)
|
|
342
|
+
if ((body as { afterCursor?: unknown }).afterCursor === true) {
|
|
343
|
+
holdSnapshotsAfterCursor.add(namespace)
|
|
344
|
+
} else {
|
|
345
|
+
holdSnapshotsAfterCursor.delete(namespace)
|
|
346
|
+
}
|
|
284
347
|
} else {
|
|
285
348
|
heldSnapshots.delete(namespace)
|
|
349
|
+
holdSnapshotsAfterCursor.delete(namespace)
|
|
286
350
|
}
|
|
351
|
+
if ((body as { reset?: unknown }).reset === true)
|
|
352
|
+
snapshotLimits.set(namespace, [])
|
|
287
353
|
return Response.json({
|
|
288
354
|
active: activeSnapshots.has(namespace),
|
|
289
355
|
held: heldSnapshots.has(namespace),
|
|
356
|
+
afterCursor: holdSnapshotsAfterCursor.has(namespace),
|
|
357
|
+
limits: snapshotLimits.get(namespace) ?? [],
|
|
290
358
|
})
|
|
291
359
|
})
|
|
292
360
|
}
|
|
@@ -337,6 +405,7 @@ export default {
|
|
|
337
405
|
delegatedFailuresRemaining,
|
|
338
406
|
delegatedPushFailedRemaining,
|
|
339
407
|
delegatedAttempts,
|
|
408
|
+
delegatedUrl,
|
|
340
409
|
})
|
|
341
410
|
)
|
|
342
411
|
}
|
|
@@ -353,10 +422,12 @@ export default {
|
|
|
353
422
|
Number((body as { pushFailed?: unknown }).pushFailed) || 0
|
|
354
423
|
)
|
|
355
424
|
delegatedAttempts = 0
|
|
425
|
+
delegatedUrl = ''
|
|
356
426
|
return Response.json({
|
|
357
427
|
delegatedFailuresRemaining,
|
|
358
428
|
delegatedPushFailedRemaining,
|
|
359
429
|
delegatedAttempts,
|
|
430
|
+
delegatedUrl,
|
|
360
431
|
})
|
|
361
432
|
})
|
|
362
433
|
}
|
package/src/types.ts
CHANGED
|
@@ -141,8 +141,10 @@ export type SyncHostConfig<Env extends SyncHostEnv = SyncHostEnv> = {
|
|
|
141
141
|
hostVersion: string
|
|
142
142
|
schema: ZeroSchemaConfig
|
|
143
143
|
mutators?: MutatorRegistry
|
|
144
|
-
/**
|
|
144
|
+
/** Absolute app push path on the upstream service binding. */
|
|
145
145
|
mutateUrl?: string
|
|
146
|
+
/** Absolute origin used for delegated push requests through the service binding. */
|
|
147
|
+
mutateOrigin?: string
|
|
146
148
|
/** Env binding for delegated pushes; defaults to upstream.binding. */
|
|
147
149
|
mutateBinding?: string
|
|
148
150
|
delegatedPushRetry?: DelegatedPushRetryConfig
|
|
@@ -154,6 +156,12 @@ export type SyncHostConfig<Env extends SyncHostEnv = SyncHostEnv> = {
|
|
|
154
156
|
request: Request,
|
|
155
157
|
env: Env
|
|
156
158
|
): NormalizedClaims | null | Promise<NormalizedClaims | null>
|
|
159
|
+
/** Authorize the advisory wake socket before selecting a namespace DO.
|
|
160
|
+
* Browser clients should present a short-lived, namespace-scoped capability
|
|
161
|
+
* in the query string because WebSocket cannot set request headers. */
|
|
162
|
+
authorizeWake(request: Request, env: Env): boolean | Promise<boolean>
|
|
163
|
+
/** Authorize upstream service notifications before selecting a namespace DO. */
|
|
164
|
+
authorizeNotify(request: Request, env: Env): boolean | Promise<boolean>
|
|
157
165
|
/** Resolve the first path component or another consumer-defined namespace. */
|
|
158
166
|
namespace(request: Request): string | null
|
|
159
167
|
visibility?: VisibilityConfig
|
package/src/write-safeguards.ts
CHANGED
|
@@ -187,22 +187,21 @@ export function trackBillableCursorRows<Cursor extends BillableCursor>(
|
|
|
187
187
|
accountedRows = current
|
|
188
188
|
record(delta)
|
|
189
189
|
}
|
|
190
|
-
const wrapIterator = (iterator:
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
return (...args
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
} finally {
|
|
199
|
-
account()
|
|
200
|
-
}
|
|
201
|
-
}
|
|
190
|
+
const wrapIterator = (iterator: Iterator<unknown>): IterableIterator<unknown> => {
|
|
191
|
+
const next = iterator.next.bind(iterator)
|
|
192
|
+
return {
|
|
193
|
+
next(...args: [] | [unknown]) {
|
|
194
|
+
try {
|
|
195
|
+
return next(...args)
|
|
196
|
+
} finally {
|
|
197
|
+
account()
|
|
202
198
|
}
|
|
203
|
-
return typeof value === 'function' ? value.bind(target) : value
|
|
204
199
|
},
|
|
205
|
-
|
|
200
|
+
[Symbol.iterator]() {
|
|
201
|
+
return this
|
|
202
|
+
},
|
|
203
|
+
}
|
|
204
|
+
}
|
|
206
205
|
let proxy: Cursor
|
|
207
206
|
proxy = new Proxy(cursor, {
|
|
208
207
|
get(target, property) {
|
|
@@ -222,7 +221,7 @@ export function trackBillableCursorRows<Cursor extends BillableCursor>(
|
|
|
222
221
|
}
|
|
223
222
|
if (property === 'raw' && typeof value === 'function') {
|
|
224
223
|
return (...args: unknown[]) =>
|
|
225
|
-
wrapIterator(Reflect.apply(value, target, args) as
|
|
224
|
+
wrapIterator(Reflect.apply(value, target, args) as Iterator<unknown>)
|
|
226
225
|
}
|
|
227
226
|
return typeof value === 'function' ? value.bind(target) : value
|
|
228
227
|
},
|