orez-sync-cf-host 0.4.53 → 0.4.55
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/config.ts +1 -1
- package/src/host.ts +79 -32
- package/src/ingest-harness-worker.ts +7 -2
- package/src/sql-storage-adapter.ts +23 -6
- package/src/types.ts +1 -1
- package/src/write-safeguards.ts +104 -12
package/package.json
CHANGED
package/src/config.ts
CHANGED
|
@@ -46,7 +46,7 @@ export function validateSyncHostConfig<Env extends SyncHostEnv>(
|
|
|
46
46
|
throw new TypeError('upstream.intervalMs must be a safe integer >= 1000')
|
|
47
47
|
}
|
|
48
48
|
for (const [name, value] of Object.entries({
|
|
49
|
-
ingestBudgetRows: config.upstream.ingestBudgetRows ??
|
|
49
|
+
ingestBudgetRows: config.upstream.ingestBudgetRows ?? 150_000,
|
|
50
50
|
ingestBudgetWindowMs: config.upstream.ingestBudgetWindowMs ?? 300_000,
|
|
51
51
|
ingestBackoffMs: config.upstream.ingestBackoffMs ?? 1_000,
|
|
52
52
|
ingestMaxBackoffMs: config.upstream.ingestMaxBackoffMs ?? 60_000,
|
package/src/host.ts
CHANGED
|
@@ -286,7 +286,7 @@ export function createSyncDurableObject<Env extends SyncHostEnv>(
|
|
|
286
286
|
const wakeCoalesceMs = config.wakeCoalesceMs ?? 25
|
|
287
287
|
const upstreamIntervalMs = config.upstream?.intervalMs ?? 15_000
|
|
288
288
|
const upstreamLimit = config.upstream?.changeLimit ?? 1_000
|
|
289
|
-
const ingestBudgetRows = config.upstream?.ingestBudgetRows ??
|
|
289
|
+
const ingestBudgetRows = config.upstream?.ingestBudgetRows ?? 150_000
|
|
290
290
|
const ingestBudgetWindowMs = config.upstream?.ingestBudgetWindowMs ?? 5 * 60_000
|
|
291
291
|
const ingestBackoffMs = config.upstream?.ingestBackoffMs ?? 1_000
|
|
292
292
|
const ingestMaxBackoffMs = config.upstream?.ingestMaxBackoffMs ?? 60_000
|
|
@@ -309,6 +309,7 @@ export function createSyncDurableObject<Env extends SyncHostEnv>(
|
|
|
309
309
|
#wakeRecipients = new Set<WebSocket>()
|
|
310
310
|
#wakePromise: Promise<void> | null = null
|
|
311
311
|
#ingestPromise: Promise<number> | null = null
|
|
312
|
+
#recordingIngestBillable = false
|
|
312
313
|
#ingestBreaker = new IngestCircuitBreaker({
|
|
313
314
|
budgetRows: ingestBudgetRows,
|
|
314
315
|
windowMs: ingestBudgetWindowMs,
|
|
@@ -319,8 +320,11 @@ export function createSyncDurableObject<Env extends SyncHostEnv>(
|
|
|
319
320
|
|
|
320
321
|
constructor(ctx: DurableObjectState, env: Env) {
|
|
321
322
|
super(ctx, env)
|
|
322
|
-
|
|
323
|
-
|
|
323
|
+
const recordRowsWritten = (rows: number) => {
|
|
324
|
+
if (this.#recordingIngestBillable) this.#ingestBreaker.recordBillable(rows)
|
|
325
|
+
}
|
|
326
|
+
this.#engineDb = new SqlStorageSyncDb(ctx.storage.sql, recordRowsWritten)
|
|
327
|
+
this.#directSql = new SqlStorageDirect(ctx.storage.sql, recordRowsWritten)
|
|
324
328
|
this.#mutatorSql = new SqlStorageMutatorTransaction(this.#directSql, (ast) =>
|
|
325
329
|
this.#wasm(() => engine_compile_query(config.schema, ast))
|
|
326
330
|
)
|
|
@@ -499,6 +503,26 @@ export function createSyncDurableObject<Env extends SyncHostEnv>(
|
|
|
499
503
|
}
|
|
500
504
|
}
|
|
501
505
|
|
|
506
|
+
async #upstreamWriteBudgetStatus(): Promise<Response> {
|
|
507
|
+
if (!config.upstream) return json({ error: 'upstream is not configured' }, 404)
|
|
508
|
+
const path = this.#controlGet('upstreamPath')
|
|
509
|
+
if (!path) return json({ error: 'upstream path is not known yet' }, 409)
|
|
510
|
+
const endpoint = new URL(`${path}/_orez/write-budget`, 'https://upstream.invalid')
|
|
511
|
+
const response = await this.#serviceBinding().fetch(endpoint.toString(), {
|
|
512
|
+
headers: { host: endpoint.host },
|
|
513
|
+
})
|
|
514
|
+
if (!response.ok) {
|
|
515
|
+
return json(
|
|
516
|
+
{
|
|
517
|
+
error: 'upstream write-budget status unavailable',
|
|
518
|
+
upstreamStatus: response.status,
|
|
519
|
+
},
|
|
520
|
+
502
|
|
521
|
+
)
|
|
522
|
+
}
|
|
523
|
+
return json(await response.json())
|
|
524
|
+
}
|
|
525
|
+
|
|
502
526
|
#rememberUpstreamPath(request: Request): string | null {
|
|
503
527
|
if (!config.upstream) return null
|
|
504
528
|
const path = request.headers.get(UPSTREAM_PATH_HEADER)
|
|
@@ -530,21 +554,31 @@ export function createSyncDurableObject<Env extends SyncHostEnv>(
|
|
|
530
554
|
}
|
|
531
555
|
}
|
|
532
556
|
|
|
533
|
-
#
|
|
557
|
+
#recordIngestLogicalRows(rows: number): void {
|
|
558
|
+
this.#ingestBreaker.recordLogical(rows)
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
#withIngestBilling<T>(fields: Record<string, unknown>, apply: () => T): T {
|
|
562
|
+
this.#recordingIngestBillable = true
|
|
534
563
|
try {
|
|
535
|
-
|
|
564
|
+
return apply()
|
|
536
565
|
} catch (error) {
|
|
537
|
-
this.#
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
566
|
+
this.#recordingIngestBillable = false
|
|
567
|
+
if (error instanceof IngestBreakerError) {
|
|
568
|
+
this.#persistIngestBreaker()
|
|
569
|
+
const status = this.#ingestBreaker.status()
|
|
570
|
+
console.error(
|
|
571
|
+
JSON.stringify({
|
|
572
|
+
event: 'sync_upstream_ingest_breaker_tripped',
|
|
573
|
+
...status,
|
|
574
|
+
reason: 'ingestBudgetExceeded',
|
|
575
|
+
...fields,
|
|
576
|
+
})
|
|
577
|
+
)
|
|
578
|
+
}
|
|
547
579
|
throw error
|
|
580
|
+
} finally {
|
|
581
|
+
this.#recordingIngestBillable = false
|
|
548
582
|
}
|
|
549
583
|
}
|
|
550
584
|
|
|
@@ -574,17 +608,21 @@ export function createSyncDurableObject<Env extends SyncHostEnv>(
|
|
|
574
608
|
throw new Error(`upstream snapshot returned ${snapshotResponse.status}`)
|
|
575
609
|
}
|
|
576
610
|
const snapshot = await snapshotResponse.json()
|
|
577
|
-
const rebuilt = this
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
611
|
+
const rebuilt = this.#withIngestBilling(
|
|
612
|
+
{ phase: 'snapshot', cursor },
|
|
613
|
+
() =>
|
|
614
|
+
this.ctx.storage.transactionSync(() =>
|
|
615
|
+
this.#wasm(() =>
|
|
616
|
+
engine_apply_upstream_snapshot(
|
|
617
|
+
this.#engineDb,
|
|
618
|
+
config.schema,
|
|
619
|
+
snapshot
|
|
620
|
+
)
|
|
621
|
+
)
|
|
622
|
+
) as ApplyUpstreamResult
|
|
623
|
+
)
|
|
582
624
|
total += rebuilt.applied
|
|
583
|
-
this.#
|
|
584
|
-
phase: 'snapshot',
|
|
585
|
-
cursor,
|
|
586
|
-
resultWatermark: rebuilt.watermark,
|
|
587
|
-
})
|
|
625
|
+
this.#recordIngestLogicalRows(rebuilt.applied)
|
|
588
626
|
const nextCursor = this.#engineState()?.upstreamWatermark ?? cursor
|
|
589
627
|
if (String(nextCursor) === String(cursor)) {
|
|
590
628
|
this.#tripIngest('ingestCursorStalled', {
|
|
@@ -603,15 +641,22 @@ export function createSyncDurableObject<Env extends SyncHostEnv>(
|
|
|
603
641
|
if (!Number.isSafeInteger(batch.watermark) || !Array.isArray(batch.changes)) {
|
|
604
642
|
throw new Error('invalid upstream changes response')
|
|
605
643
|
}
|
|
606
|
-
const result = this
|
|
607
|
-
|
|
644
|
+
const result = this.#withIngestBilling(
|
|
645
|
+
{
|
|
646
|
+
phase: 'changes',
|
|
647
|
+
cursor,
|
|
648
|
+
batchWatermark: batch.watermark,
|
|
649
|
+
changeRows: batch.changes.length,
|
|
650
|
+
},
|
|
651
|
+
() =>
|
|
652
|
+
this.ctx.storage.transactionSync(() =>
|
|
653
|
+
this.#wasm(() =>
|
|
654
|
+
engine_apply_upstream(this.#engineDb, config.schema, batch)
|
|
655
|
+
)
|
|
656
|
+
)
|
|
608
657
|
) as ApplyUpstreamResult
|
|
609
658
|
total += result.applied
|
|
610
|
-
this.#
|
|
611
|
-
phase: 'changes',
|
|
612
|
-
cursor,
|
|
613
|
-
batchWatermark: batch.watermark,
|
|
614
|
-
})
|
|
659
|
+
this.#recordIngestLogicalRows(result.applied)
|
|
615
660
|
const nextCursor = this.#engineState()?.upstreamWatermark ?? cursor
|
|
616
661
|
if (batch.changes.length > 0 && String(nextCursor) === String(cursor)) {
|
|
617
662
|
this.#tripIngest('ingestCursorStalled', {
|
|
@@ -1258,6 +1303,8 @@ export function createSyncDurableObject<Env extends SyncHostEnv>(
|
|
|
1258
1303
|
|
|
1259
1304
|
#admin(route: string, request: Request): Promise<Response> | Response {
|
|
1260
1305
|
if (route === '/admin/health') return json({ ok: true })
|
|
1306
|
+
if (route === '/admin/upstream-write-budget' && request.method === 'GET')
|
|
1307
|
+
return this.#upstreamWriteBudgetStatus()
|
|
1261
1308
|
if (route === '/admin/status') {
|
|
1262
1309
|
const heap = (
|
|
1263
1310
|
performance as Performance & {
|
|
@@ -70,6 +70,8 @@ async function upstreamFetch(request: Request, env: Env): Promise<Response> {
|
|
|
70
70
|
const [, namespace, ...rest] = url.pathname.split('/')
|
|
71
71
|
if (!namespace) return new Response('namespace required', { status: 400 })
|
|
72
72
|
const stub = env.UPSTREAM_DO.get(env.UPSTREAM_DO.idFromName(namespace))
|
|
73
|
+
url.pathname = `/${rest.join('/')}`
|
|
74
|
+
if (url.pathname === '/_orez/write-budget') return stub.fetch(new Request(url, request))
|
|
73
75
|
const exec = async (sql: string, params: unknown[] = []) => {
|
|
74
76
|
const response = await stub.fetch('https://upstream.invalid/exec', {
|
|
75
77
|
method: 'POST',
|
|
@@ -88,7 +90,6 @@ async function upstreamFetch(request: Request, env: Env): Promise<Response> {
|
|
|
88
90
|
'INSERT OR IGNORE INTO _zero_schema_tables (name, schema_json) VALUES (?, ?)',
|
|
89
91
|
['item', JSON.stringify(schema.tables.item)]
|
|
90
92
|
)
|
|
91
|
-
url.pathname = `/${rest.join('/')}`
|
|
92
93
|
return stub.fetch(new Request(url, request))
|
|
93
94
|
}
|
|
94
95
|
|
|
@@ -97,7 +98,11 @@ export class DataService extends WorkerEntrypoint<Env> {
|
|
|
97
98
|
fetch(request: Request): Promise<Response> {
|
|
98
99
|
const url = new URL(request.url)
|
|
99
100
|
const pathname = url.pathname
|
|
100
|
-
if (
|
|
101
|
+
if (
|
|
102
|
+
!pathname.endsWith('/changes') &&
|
|
103
|
+
!pathname.endsWith('/snapshot') &&
|
|
104
|
+
!pathname.endsWith('/_orez/write-budget')
|
|
105
|
+
) {
|
|
101
106
|
return Promise.resolve(
|
|
102
107
|
new Response('DATA route rejected non-feed request', { status: 418 })
|
|
103
108
|
)
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { trackBillableCursorRows } from './write-safeguards.js'
|
|
2
|
+
|
|
1
3
|
import type { MutatorSql, SyncSql } from './types.js'
|
|
2
4
|
|
|
3
5
|
type WireValue =
|
|
@@ -86,7 +88,10 @@ function encodeResult(value: unknown): WireValue {
|
|
|
86
88
|
export class SqlStorageSyncDb implements JsSyncDb {
|
|
87
89
|
readonly stats: AdapterStats = { execCalls: 0, queryCalls: 0, sqlMs: 0 }
|
|
88
90
|
|
|
89
|
-
constructor(
|
|
91
|
+
constructor(
|
|
92
|
+
private readonly sql: SqlStorage,
|
|
93
|
+
private readonly recordRowsWritten: (rows: number) => void = () => {}
|
|
94
|
+
) {}
|
|
90
95
|
|
|
91
96
|
resetStats(): void {
|
|
92
97
|
this.stats.execCalls = 0
|
|
@@ -97,7 +102,10 @@ export class SqlStorageSyncDb implements JsSyncDb {
|
|
|
97
102
|
exec(sql: string, params: WireValue[]): void {
|
|
98
103
|
assertHostSql(sql)
|
|
99
104
|
const start = performance.now()
|
|
100
|
-
|
|
105
|
+
trackBillableCursorRows(
|
|
106
|
+
this.sql.exec(sql, ...params.map(decodeBinding)),
|
|
107
|
+
this.recordRowsWritten
|
|
108
|
+
)
|
|
101
109
|
this.stats.execCalls++
|
|
102
110
|
this.stats.sqlMs += performance.now() - start
|
|
103
111
|
}
|
|
@@ -105,7 +113,10 @@ export class SqlStorageSyncDb implements JsSyncDb {
|
|
|
105
113
|
query(sql: string, params: WireValue[]): WireRow[] {
|
|
106
114
|
assertHostSql(sql)
|
|
107
115
|
const start = performance.now()
|
|
108
|
-
const cursor =
|
|
116
|
+
const cursor = trackBillableCursorRows(
|
|
117
|
+
this.sql.exec(sql, ...params.map(decodeBinding)),
|
|
118
|
+
this.recordRowsWritten
|
|
119
|
+
)
|
|
109
120
|
const columns = [...cursor.columnNames]
|
|
110
121
|
// A SqlStorage cursor must be fully consumed before any await. Returning a
|
|
111
122
|
// materialized array here makes that property structural.
|
|
@@ -121,11 +132,14 @@ export class SqlStorageSyncDb implements JsSyncDb {
|
|
|
121
132
|
|
|
122
133
|
/** Direct application SQL surface used for initialization and admin reads. */
|
|
123
134
|
export class SqlStorageDirect implements SyncSql {
|
|
124
|
-
constructor(
|
|
135
|
+
constructor(
|
|
136
|
+
private readonly sql: SqlStorage,
|
|
137
|
+
private readonly recordRowsWritten: (rows: number) => void = () => {}
|
|
138
|
+
) {}
|
|
125
139
|
|
|
126
140
|
exec(sql: string, params: readonly unknown[] = []): void {
|
|
127
141
|
assertHostSql(sql)
|
|
128
|
-
this.sql.exec(sql, ...params)
|
|
142
|
+
trackBillableCursorRows(this.sql.exec(sql, ...params), this.recordRowsWritten)
|
|
129
143
|
}
|
|
130
144
|
|
|
131
145
|
query<Row extends Record<string, unknown> = Record<string, unknown>>(
|
|
@@ -133,7 +147,10 @@ export class SqlStorageDirect implements SyncSql {
|
|
|
133
147
|
params: readonly unknown[] = []
|
|
134
148
|
): Row[] {
|
|
135
149
|
assertHostSql(sql)
|
|
136
|
-
return
|
|
150
|
+
return trackBillableCursorRows(
|
|
151
|
+
this.sql.exec(sql, ...params),
|
|
152
|
+
this.recordRowsWritten
|
|
153
|
+
).toArray() as Row[]
|
|
137
154
|
}
|
|
138
155
|
}
|
|
139
156
|
|
package/src/types.ts
CHANGED
|
@@ -116,7 +116,7 @@ export type UpstreamConfig = {
|
|
|
116
116
|
changeLimit?: number
|
|
117
117
|
/** Durable Object alarm safety net. Defaults to 15 seconds. */
|
|
118
118
|
intervalMs?: number
|
|
119
|
-
/**
|
|
119
|
+
/** Billable SQLite rows written by ingest per rolling window. Defaults to 150,000. */
|
|
120
120
|
ingestBudgetRows?: number
|
|
121
121
|
/** Rolling ingest budget window. Defaults to five minutes. */
|
|
122
122
|
ingestBudgetWindowMs?: number
|
package/src/write-safeguards.ts
CHANGED
|
@@ -10,6 +10,8 @@ export type IngestBreakerOptions = {
|
|
|
10
10
|
|
|
11
11
|
export type IngestBreakerStatus = {
|
|
12
12
|
windowRows: number
|
|
13
|
+
billableRows: number
|
|
14
|
+
logicalRows: number
|
|
13
15
|
budget: number
|
|
14
16
|
windowMs: number
|
|
15
17
|
tripped: boolean
|
|
@@ -19,7 +21,7 @@ export type IngestBreakerStatus = {
|
|
|
19
21
|
consecutiveTrips: number
|
|
20
22
|
}
|
|
21
23
|
|
|
22
|
-
type Sample = { at: number;
|
|
24
|
+
type Sample = { at: number; billableRows: number; logicalRows: number }
|
|
23
25
|
|
|
24
26
|
export class IngestBreakerError extends Error {
|
|
25
27
|
readonly status = 429
|
|
@@ -38,7 +40,8 @@ export class IngestBreakerError extends Error {
|
|
|
38
40
|
export class IngestCircuitBreaker {
|
|
39
41
|
readonly #options: IngestBreakerOptions
|
|
40
42
|
#samples: Sample[] = []
|
|
41
|
-
#
|
|
43
|
+
#billableRows = 0
|
|
44
|
+
#logicalRows = 0
|
|
42
45
|
#reason: IngestBreakerReason | null = null
|
|
43
46
|
#retryAt: number | null = null
|
|
44
47
|
#consecutiveTrips = 0
|
|
@@ -56,7 +59,8 @@ export class IngestCircuitBreaker {
|
|
|
56
59
|
const cutoff = now - this.#options.windowMs
|
|
57
60
|
let remove = 0
|
|
58
61
|
while (remove < this.#samples.length && this.#samples[remove]!.at <= cutoff) {
|
|
59
|
-
this.#
|
|
62
|
+
this.#billableRows -= this.#samples[remove]!.billableRows
|
|
63
|
+
this.#logicalRows -= this.#samples[remove]!.logicalRows
|
|
60
64
|
remove++
|
|
61
65
|
}
|
|
62
66
|
if (remove > 0) this.#samples.splice(0, remove)
|
|
@@ -67,7 +71,9 @@ export class IngestCircuitBreaker {
|
|
|
67
71
|
this.#prune(now)
|
|
68
72
|
const retryAfterMs = Math.max(0, (this.#retryAt ?? now) - now)
|
|
69
73
|
return {
|
|
70
|
-
windowRows: this.#
|
|
74
|
+
windowRows: this.#billableRows,
|
|
75
|
+
billableRows: this.#billableRows,
|
|
76
|
+
logicalRows: this.#logicalRows,
|
|
71
77
|
budget: this.#options.budgetRows,
|
|
72
78
|
windowMs: this.#options.windowMs,
|
|
73
79
|
tripped: this.#reason !== null && retryAfterMs > 0,
|
|
@@ -91,15 +97,34 @@ export class IngestCircuitBreaker {
|
|
|
91
97
|
}
|
|
92
98
|
|
|
93
99
|
record(rows: number): void {
|
|
100
|
+
this.recordLogical(rows)
|
|
101
|
+
this.recordBillable(rows)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
#sample(now: number): Sample {
|
|
105
|
+
this.#prune(now)
|
|
106
|
+
const last = this.#samples[this.#samples.length - 1]
|
|
107
|
+
if (last?.at === now) return last
|
|
108
|
+
const sample = { at: now, billableRows: 0, logicalRows: 0 }
|
|
109
|
+
this.#samples.push(sample)
|
|
110
|
+
return sample
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
recordLogical(rows: number): void {
|
|
114
|
+
if (!Number.isSafeInteger(rows) || rows <= 0) return
|
|
115
|
+
const sample = this.#sample(this.#options.now())
|
|
116
|
+
sample.logicalRows += rows
|
|
117
|
+
this.#logicalRows += rows
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
recordBillable(rows: number): void {
|
|
94
121
|
this.assertReady()
|
|
95
122
|
if (!Number.isSafeInteger(rows) || rows <= 0) return
|
|
96
123
|
const now = this.#options.now()
|
|
97
|
-
this.#
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
this.#windowRows += rows
|
|
102
|
-
if (this.#windowRows > this.#options.budgetRows) this.trip('ingestBudgetExceeded')
|
|
124
|
+
const sample = this.#sample(now)
|
|
125
|
+
sample.billableRows += rows
|
|
126
|
+
this.#billableRows += rows
|
|
127
|
+
if (this.#billableRows > this.#options.budgetRows) this.trip('ingestBudgetExceeded')
|
|
103
128
|
}
|
|
104
129
|
|
|
105
130
|
trip(reason: IngestBreakerReason): never {
|
|
@@ -113,7 +138,7 @@ export class IngestCircuitBreaker {
|
|
|
113
138
|
this.#retryAt = now + delay
|
|
114
139
|
throw new IngestBreakerError(
|
|
115
140
|
reason,
|
|
116
|
-
this.#
|
|
141
|
+
this.#billableRows,
|
|
117
142
|
this.#options.budgetRows,
|
|
118
143
|
delay
|
|
119
144
|
)
|
|
@@ -134,11 +159,78 @@ export class IngestCircuitBreaker {
|
|
|
134
159
|
|
|
135
160
|
reopen(): void {
|
|
136
161
|
this.#samples = []
|
|
137
|
-
this.#
|
|
162
|
+
this.#billableRows = 0
|
|
163
|
+
this.#logicalRows = 0
|
|
138
164
|
this.recovered()
|
|
139
165
|
}
|
|
140
166
|
}
|
|
141
167
|
|
|
168
|
+
type BillableCursor = {
|
|
169
|
+
rowsWritten?: number
|
|
170
|
+
next?: (...args: unknown[]) => unknown
|
|
171
|
+
one?: (...args: unknown[]) => unknown
|
|
172
|
+
toArray?: (...args: unknown[]) => unknown
|
|
173
|
+
raw?: (...args: unknown[]) => unknown
|
|
174
|
+
[Symbol.iterator]?: () => Iterator<unknown>
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function trackBillableCursorRows<Cursor extends BillableCursor>(
|
|
178
|
+
cursor: Cursor,
|
|
179
|
+
record: (rows: number) => void
|
|
180
|
+
): Cursor {
|
|
181
|
+
if (!cursor || typeof cursor !== 'object') return cursor
|
|
182
|
+
let accountedRows = 0
|
|
183
|
+
const account = () => {
|
|
184
|
+
const current = Number(cursor.rowsWritten ?? 0)
|
|
185
|
+
if (!Number.isSafeInteger(current) || current <= accountedRows) return
|
|
186
|
+
const delta = current - accountedRows
|
|
187
|
+
accountedRows = current
|
|
188
|
+
record(delta)
|
|
189
|
+
}
|
|
190
|
+
const wrapIterator = (iterator: object): object =>
|
|
191
|
+
new Proxy(iterator, {
|
|
192
|
+
get(target, property) {
|
|
193
|
+
const value = Reflect.get(target, property, target)
|
|
194
|
+
if (property === 'next' && typeof value === 'function') {
|
|
195
|
+
return (...args: unknown[]) => {
|
|
196
|
+
try {
|
|
197
|
+
return Reflect.apply(value, target, args)
|
|
198
|
+
} finally {
|
|
199
|
+
account()
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return typeof value === 'function' ? value.bind(target) : value
|
|
204
|
+
},
|
|
205
|
+
})
|
|
206
|
+
let proxy: Cursor
|
|
207
|
+
proxy = new Proxy(cursor, {
|
|
208
|
+
get(target, property) {
|
|
209
|
+
const value = Reflect.get(target, property, target)
|
|
210
|
+
if (property === Symbol.iterator) return () => proxy as unknown as Iterator<unknown>
|
|
211
|
+
if (
|
|
212
|
+
(property === 'next' || property === 'one' || property === 'toArray') &&
|
|
213
|
+
typeof value === 'function'
|
|
214
|
+
) {
|
|
215
|
+
return (...args: unknown[]) => {
|
|
216
|
+
try {
|
|
217
|
+
return Reflect.apply(value, target, args)
|
|
218
|
+
} finally {
|
|
219
|
+
account()
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (property === 'raw' && typeof value === 'function') {
|
|
224
|
+
return (...args: unknown[]) =>
|
|
225
|
+
wrapIterator(Reflect.apply(value, target, args) as object)
|
|
226
|
+
}
|
|
227
|
+
return typeof value === 'function' ? value.bind(target) : value
|
|
228
|
+
},
|
|
229
|
+
})
|
|
230
|
+
account()
|
|
231
|
+
return proxy
|
|
232
|
+
}
|
|
233
|
+
|
|
142
234
|
export function retryDelayMs(
|
|
143
235
|
attempt: number,
|
|
144
236
|
initialBackoffMs: number,
|