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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "orez-sync-cf-host",
3
- "version": "0.4.63",
3
+ "version": "0.4.65",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./src/index.ts"
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(delegateTimeoutMs),
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
- mutations?: Array<{ id?: { clientID?: unknown; id?: unknown } }>
1058
- pushResponse?: {
1059
- mutations?: Array<{ id?: { clientID?: unknown; id?: unknown } }>
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?.mutations ?? upstreamBody.mutations
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: upstreamBody.pushResponse ?? upstreamBody })
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
- return upstreamFetch(request, this.env)
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({ delegatedFailuresRemaining, delegatedAttempts })
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({ delegatedFailuresRemaining, delegatedAttempts })
272
+ return Response.json({
273
+ delegatedFailuresRemaining,
274
+ delegatedPushFailedRemaining,
275
+ delegatedAttempts,
276
+ })
243
277
  })
244
278
  }
245
279
  if (url.pathname.startsWith('/upstream/')) {