orez-sync-cf-host 0.4.63 → 0.4.65
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/package.json +1 -1
- package/src/host.ts +51 -10
- package/src/ingest-harness-worker.ts +38 -4
package/package.json
CHANGED
package/src/host.ts
CHANGED
|
@@ -73,6 +73,13 @@ type MutationResult = {
|
|
|
73
73
|
result: Record<string, unknown>
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
type DelegatedMutationResult = { id?: { clientID?: unknown; id?: unknown } }
|
|
77
|
+
type DelegatedPushBody = {
|
|
78
|
+
mutations?: DelegatedMutationResult[]
|
|
79
|
+
pushResponse?: unknown
|
|
80
|
+
[key: string]: unknown
|
|
81
|
+
}
|
|
82
|
+
|
|
76
83
|
type EngineState = { watermark: string; floor: string; upstreamWatermark: string }
|
|
77
84
|
type UpstreamBatch = {
|
|
78
85
|
watermark: number
|
|
@@ -135,6 +142,18 @@ function json(value: unknown, status = 200): Response {
|
|
|
135
142
|
})
|
|
136
143
|
}
|
|
137
144
|
|
|
145
|
+
function isStructuredPushFailed(value: unknown): value is Record<string, unknown> {
|
|
146
|
+
if (typeof value !== 'object' || value === null) return false
|
|
147
|
+
const body = value as Record<string, unknown>
|
|
148
|
+
return (
|
|
149
|
+
body.kind === 'PushFailed' &&
|
|
150
|
+
typeof body.origin === 'string' &&
|
|
151
|
+
typeof body.reason === 'string' &&
|
|
152
|
+
typeof body.message === 'string' &&
|
|
153
|
+
Array.isArray(body.mutationIDs)
|
|
154
|
+
)
|
|
155
|
+
}
|
|
156
|
+
|
|
138
157
|
function statusOf(error: unknown): number {
|
|
139
158
|
if (typeof error === 'object' && error !== null && 'status' in error) {
|
|
140
159
|
const status = Number(error.status)
|
|
@@ -690,7 +709,8 @@ export function createSyncDurableObject<Env extends SyncHostEnv>(
|
|
|
690
709
|
async #fetchDelegatedPush(
|
|
691
710
|
endpoint: URL,
|
|
692
711
|
headers: Headers,
|
|
693
|
-
body: ArrayBuffer
|
|
712
|
+
body: ArrayBuffer,
|
|
713
|
+
provisioning = false
|
|
694
714
|
): Promise<Response> {
|
|
695
715
|
const binding = this.#serviceBinding(
|
|
696
716
|
config.mutateBinding ?? config.upstream?.binding
|
|
@@ -703,7 +723,9 @@ export function createSyncDurableObject<Env extends SyncHostEnv>(
|
|
|
703
723
|
method: 'POST',
|
|
704
724
|
headers,
|
|
705
725
|
body,
|
|
706
|
-
signal: AbortSignal.timeout(
|
|
726
|
+
signal: AbortSignal.timeout(
|
|
727
|
+
provisioning ? Math.max(delegateTimeoutMs, 25_000) : delegateTimeoutMs
|
|
728
|
+
),
|
|
707
729
|
})
|
|
708
730
|
} catch (error) {
|
|
709
731
|
lastError = error
|
|
@@ -1048,19 +1070,27 @@ export function createSyncDurableObject<Env extends SyncHostEnv>(
|
|
|
1048
1070
|
const upstreamResponse = await this.#fetchDelegatedPush(
|
|
1049
1071
|
endpoint,
|
|
1050
1072
|
headers,
|
|
1051
|
-
bytes
|
|
1073
|
+
bytes,
|
|
1074
|
+
this.#engineState()?.upstreamWatermark === '0'
|
|
1052
1075
|
)
|
|
1053
1076
|
if (!upstreamResponse.ok) {
|
|
1054
1077
|
return new Response(upstreamResponse.body, upstreamResponse)
|
|
1055
1078
|
}
|
|
1056
|
-
const upstreamBody = (await upstreamResponse.json()) as
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1079
|
+
const upstreamBody = (await upstreamResponse.json()) as DelegatedPushBody
|
|
1080
|
+
const delegatedResponse = upstreamBody.pushResponse ?? upstreamBody
|
|
1081
|
+
if (isStructuredPushFailed(delegatedResponse)) {
|
|
1082
|
+
// PushFailed is a successful protocol response describing an
|
|
1083
|
+
// application-level failure. There are intentionally no mutation
|
|
1084
|
+
// acknowledgements to finalize in the host; preserve the body so
|
|
1085
|
+
// the Zero client can apply its retry/error policy.
|
|
1086
|
+
return json({ pushResponse: delegatedResponse })
|
|
1061
1087
|
}
|
|
1062
1088
|
const acknowledged =
|
|
1063
|
-
upstreamBody.pushResponse
|
|
1089
|
+
typeof upstreamBody.pushResponse === 'object' &&
|
|
1090
|
+
upstreamBody.pushResponse !== null &&
|
|
1091
|
+
'mutations' in upstreamBody.pushResponse
|
|
1092
|
+
? upstreamBody.pushResponse.mutations
|
|
1093
|
+
: upstreamBody.mutations
|
|
1064
1094
|
if (!Array.isArray(acknowledged)) {
|
|
1065
1095
|
throw new Error('delegated push returned no mutation results')
|
|
1066
1096
|
}
|
|
@@ -1100,7 +1130,7 @@ export function createSyncDurableObject<Env extends SyncHostEnv>(
|
|
|
1100
1130
|
)
|
|
1101
1131
|
}
|
|
1102
1132
|
await this.#ingest(upstreamPath)
|
|
1103
|
-
return json({ pushResponse:
|
|
1133
|
+
return json({ pushResponse: delegatedResponse })
|
|
1104
1134
|
} catch (error) {
|
|
1105
1135
|
const status = statusOf(error)
|
|
1106
1136
|
return json(errorBody(error), status)
|
|
@@ -1477,6 +1507,17 @@ export function createSyncDurableObject<Env extends SyncHostEnv>(
|
|
|
1477
1507
|
return this.#pull(request, claims, namespace)
|
|
1478
1508
|
}
|
|
1479
1509
|
if (route === '/push' && request.method === 'POST') {
|
|
1510
|
+
// A push may be the first request for a fresh namespace. DATA's
|
|
1511
|
+
// /changes call is also its schema-provisioning barrier, so complete it
|
|
1512
|
+
// before delegating the mutation to APP. Pull already enforces this
|
|
1513
|
+
// ordering above; skipping it here let APP race half-created tables and
|
|
1514
|
+
// surface a terminal, bodyless 500 to Zero.
|
|
1515
|
+
try {
|
|
1516
|
+
await this.#ingest(upstreamPath)
|
|
1517
|
+
} catch (error) {
|
|
1518
|
+
await request.arrayBuffer()
|
|
1519
|
+
return json(errorBody(error), statusOf(error))
|
|
1520
|
+
}
|
|
1480
1521
|
return this.#push(request, claims, namespace, upstreamPath)
|
|
1481
1522
|
}
|
|
1482
1523
|
return json({ error: 'not found' }, 404)
|
|
@@ -29,8 +29,10 @@ interface Env extends SyncHostEnv {
|
|
|
29
29
|
|
|
30
30
|
const runawayNamespaces = new Set<string>()
|
|
31
31
|
const numericTextNamespaces = new Set<string>()
|
|
32
|
+
const hydratedNamespaces = new Set<string>()
|
|
32
33
|
let delegatedFailuresRemaining = 0
|
|
33
34
|
let delegatedAttempts = 0
|
|
35
|
+
let delegatedPushFailedRemaining = 0
|
|
34
36
|
|
|
35
37
|
const config: SyncHostConfig<Env> = {
|
|
36
38
|
hostVersion: 'upstream-ingest-harness',
|
|
@@ -96,7 +98,7 @@ async function upstreamFetch(request: Request, env: Env): Promise<Response> {
|
|
|
96
98
|
|
|
97
99
|
/** Self service-binding target backed by the real ZeroSqlDO. */
|
|
98
100
|
export class DataService extends WorkerEntrypoint<Env> {
|
|
99
|
-
fetch(request: Request): Promise<Response> {
|
|
101
|
+
async fetch(request: Request): Promise<Response> {
|
|
100
102
|
const url = new URL(request.url)
|
|
101
103
|
const pathname = url.pathname
|
|
102
104
|
if (
|
|
@@ -170,7 +172,9 @@ export class DataService extends WorkerEntrypoint<Env> {
|
|
|
170
172
|
})
|
|
171
173
|
)
|
|
172
174
|
}
|
|
173
|
-
|
|
175
|
+
const response = await upstreamFetch(request, this.env)
|
|
176
|
+
if (pathname.endsWith('/changes') && response.ok) hydratedNamespaces.add(namespace)
|
|
177
|
+
return response
|
|
174
178
|
}
|
|
175
179
|
}
|
|
176
180
|
|
|
@@ -181,7 +185,25 @@ export class AppService extends WorkerEntrypoint<Env> {
|
|
|
181
185
|
new Response('APP route rejected non-push request', { status: 418 })
|
|
182
186
|
)
|
|
183
187
|
}
|
|
188
|
+
const namespace = new URL(request.url).pathname.split('/')[1] ?? ''
|
|
189
|
+
if (!hydratedNamespaces.has(namespace)) {
|
|
190
|
+
return Promise.resolve(
|
|
191
|
+
Response.json({ error: 'schema provisioning has not completed' }, { status: 500 })
|
|
192
|
+
)
|
|
193
|
+
}
|
|
184
194
|
delegatedAttempts++
|
|
195
|
+
if (delegatedPushFailedRemaining > 0) {
|
|
196
|
+
delegatedPushFailedRemaining--
|
|
197
|
+
return Promise.resolve(
|
|
198
|
+
Response.json({
|
|
199
|
+
kind: 'PushFailed',
|
|
200
|
+
origin: 'server',
|
|
201
|
+
reason: 'database',
|
|
202
|
+
mutationIDs: [{ clientID: 'writer', id: 2 }],
|
|
203
|
+
message: 'synthetic mutation result persistence failure',
|
|
204
|
+
})
|
|
205
|
+
)
|
|
206
|
+
}
|
|
185
207
|
if (delegatedFailuresRemaining > 0) {
|
|
186
208
|
delegatedFailuresRemaining--
|
|
187
209
|
return Promise.resolve(
|
|
@@ -227,7 +249,11 @@ export default {
|
|
|
227
249
|
if (url.pathname === '/delegation-control') {
|
|
228
250
|
if (request.method === 'GET') {
|
|
229
251
|
return Promise.resolve(
|
|
230
|
-
Response.json({
|
|
252
|
+
Response.json({
|
|
253
|
+
delegatedFailuresRemaining,
|
|
254
|
+
delegatedPushFailedRemaining,
|
|
255
|
+
delegatedAttempts,
|
|
256
|
+
})
|
|
231
257
|
)
|
|
232
258
|
}
|
|
233
259
|
return request
|
|
@@ -238,8 +264,16 @@ export default {
|
|
|
238
264
|
0,
|
|
239
265
|
Number((body as { failures?: unknown }).failures) || 0
|
|
240
266
|
)
|
|
267
|
+
delegatedPushFailedRemaining = Math.max(
|
|
268
|
+
0,
|
|
269
|
+
Number((body as { pushFailed?: unknown }).pushFailed) || 0
|
|
270
|
+
)
|
|
241
271
|
delegatedAttempts = 0
|
|
242
|
-
return Response.json({
|
|
272
|
+
return Response.json({
|
|
273
|
+
delegatedFailuresRemaining,
|
|
274
|
+
delegatedPushFailedRemaining,
|
|
275
|
+
delegatedAttempts,
|
|
276
|
+
})
|
|
243
277
|
})
|
|
244
278
|
}
|
|
245
279
|
if (url.pathname.startsWith('/upstream/')) {
|