orez-sync-cf-host 0.4.62 → 0.4.64

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.62",
3
+ "version": "0.4.64",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./src/index.ts"
Binary file
package/src/host.ts CHANGED
@@ -690,7 +690,8 @@ export function createSyncDurableObject<Env extends SyncHostEnv>(
690
690
  async #fetchDelegatedPush(
691
691
  endpoint: URL,
692
692
  headers: Headers,
693
- body: ArrayBuffer
693
+ body: ArrayBuffer,
694
+ provisioning = false
694
695
  ): Promise<Response> {
695
696
  const binding = this.#serviceBinding(
696
697
  config.mutateBinding ?? config.upstream?.binding
@@ -703,7 +704,9 @@ export function createSyncDurableObject<Env extends SyncHostEnv>(
703
704
  method: 'POST',
704
705
  headers,
705
706
  body,
706
- signal: AbortSignal.timeout(delegateTimeoutMs),
707
+ signal: AbortSignal.timeout(
708
+ provisioning ? Math.max(delegateTimeoutMs, 25_000) : delegateTimeoutMs
709
+ ),
707
710
  })
708
711
  } catch (error) {
709
712
  lastError = error
@@ -1048,7 +1051,8 @@ export function createSyncDurableObject<Env extends SyncHostEnv>(
1048
1051
  const upstreamResponse = await this.#fetchDelegatedPush(
1049
1052
  endpoint,
1050
1053
  headers,
1051
- bytes
1054
+ bytes,
1055
+ this.#engineState()?.upstreamWatermark === '0'
1052
1056
  )
1053
1057
  if (!upstreamResponse.ok) {
1054
1058
  return new Response(upstreamResponse.body, upstreamResponse)
@@ -1477,6 +1481,17 @@ export function createSyncDurableObject<Env extends SyncHostEnv>(
1477
1481
  return this.#pull(request, claims, namespace)
1478
1482
  }
1479
1483
  if (route === '/push' && request.method === 'POST') {
1484
+ // A push may be the first request for a fresh namespace. DATA's
1485
+ // /changes call is also its schema-provisioning barrier, so complete it
1486
+ // before delegating the mutation to APP. Pull already enforces this
1487
+ // ordering above; skipping it here let APP race half-created tables and
1488
+ // surface a terminal, bodyless 500 to Zero.
1489
+ try {
1490
+ await this.#ingest(upstreamPath)
1491
+ } catch (error) {
1492
+ await request.arrayBuffer()
1493
+ return json(errorBody(error), statusOf(error))
1494
+ }
1480
1495
  return this.#push(request, claims, namespace, upstreamPath)
1481
1496
  }
1482
1497
  return json({ error: 'not found' }, 404)
@@ -28,6 +28,8 @@ interface Env extends SyncHostEnv {
28
28
  }
29
29
 
30
30
  const runawayNamespaces = new Set<string>()
31
+ const numericTextNamespaces = new Set<string>()
32
+ const hydratedNamespaces = new Set<string>()
31
33
  let delegatedFailuresRemaining = 0
32
34
  let delegatedAttempts = 0
33
35
 
@@ -95,7 +97,7 @@ async function upstreamFetch(request: Request, env: Env): Promise<Response> {
95
97
 
96
98
  /** Self service-binding target backed by the real ZeroSqlDO. */
97
99
  export class DataService extends WorkerEntrypoint<Env> {
98
- fetch(request: Request): Promise<Response> {
100
+ async fetch(request: Request): Promise<Response> {
99
101
  const url = new URL(request.url)
100
102
  const pathname = url.pathname
101
103
  if (
@@ -108,6 +110,45 @@ export class DataService extends WorkerEntrypoint<Env> {
108
110
  )
109
111
  }
110
112
  const namespace = pathname.split('/')[1] ?? ''
113
+ if (pathname.endsWith('/changes') && numericTextNamespaces.has(namespace)) {
114
+ const watermark = Number(new URL(request.url).searchParams.get('watermark') ?? 0)
115
+ return Promise.resolve(
116
+ Response.json({
117
+ watermark: 2,
118
+ changes:
119
+ watermark >= 2
120
+ ? []
121
+ : [
122
+ {
123
+ watermark: 1,
124
+ tableName: 'item',
125
+ op: 'INSERT',
126
+ rowData: {
127
+ id: 'numeric-text',
128
+ label: 'SQL timestamp text',
129
+ rank: '2026-07-11 13:34:46',
130
+ done: false,
131
+ meta: null,
132
+ },
133
+ oldData: null,
134
+ },
135
+ {
136
+ watermark: 2,
137
+ tableName: 'item',
138
+ op: 'INSERT',
139
+ rowData: {
140
+ id: 'numeric-native',
141
+ label: 'native JSON number',
142
+ rank: 1783776886000,
143
+ done: false,
144
+ meta: null,
145
+ },
146
+ oldData: null,
147
+ },
148
+ ],
149
+ })
150
+ )
151
+ }
111
152
  if (pathname.endsWith('/changes') && runawayNamespaces.has(namespace)) {
112
153
  return Promise.resolve(
113
154
  Response.json({
@@ -130,7 +171,9 @@ export class DataService extends WorkerEntrypoint<Env> {
130
171
  })
131
172
  )
132
173
  }
133
- return upstreamFetch(request, this.env)
174
+ const response = await upstreamFetch(request, this.env)
175
+ if (pathname.endsWith('/changes') && response.ok) hydratedNamespaces.add(namespace)
176
+ return response
134
177
  }
135
178
  }
136
179
 
@@ -141,6 +184,12 @@ export class AppService extends WorkerEntrypoint<Env> {
141
184
  new Response('APP route rejected non-push request', { status: 418 })
142
185
  )
143
186
  }
187
+ const namespace = new URL(request.url).pathname.split('/')[1] ?? ''
188
+ if (!hydratedNamespaces.has(namespace)) {
189
+ return Promise.resolve(
190
+ Response.json({ error: 'schema provisioning has not completed' }, { status: 500 })
191
+ )
192
+ }
144
193
  delegatedAttempts++
145
194
  if (delegatedFailuresRemaining > 0) {
146
195
  delegatedFailuresRemaining--
@@ -156,6 +205,18 @@ const syncWorker = createSyncWorker(config)
156
205
  export default {
157
206
  fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
158
207
  const url = new URL(request.url)
208
+ if (url.pathname.startsWith('/numeric-text-control/')) {
209
+ const namespace = url.pathname.slice('/numeric-text-control/'.length)
210
+ return request
211
+ .json()
212
+ .catch(() => ({}))
213
+ .then((body) => {
214
+ if ((body as { enabled?: unknown }).enabled === true)
215
+ numericTextNamespaces.add(namespace)
216
+ else numericTextNamespaces.delete(namespace)
217
+ return Response.json({ ok: true, namespace })
218
+ })
219
+ }
159
220
  if (url.pathname.startsWith('/runaway-control/')) {
160
221
  const namespace = url.pathname.slice('/runaway-control/'.length)
161
222
  return request