@electric-ax/agents-server 0.4.19 → 0.4.20
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/dist/entrypoint.js +586 -39
- package/dist/index.cjs +572 -35
- package/dist/index.d.cts +290 -40
- package/dist/index.d.ts +290 -40
- package/dist/index.js +573 -36
- package/drizzle/0015_pg_sync_bridges.sql +14 -0
- package/drizzle/meta/_journal.json +7 -0
- package/package.json +6 -6
- package/src/db/schema.ts +28 -0
- package/src/entity-manager.ts +34 -29
- package/src/entity-registry.ts +144 -3
- package/src/manifest-side-effects.ts +10 -0
- package/src/pg-sync-bridge-manager.ts +552 -0
- package/src/routing/context.ts +2 -0
- package/src/routing/entities-router.ts +28 -16
- package/src/routing/global-router.ts +3 -0
- package/src/routing/hooks.ts +7 -0
- package/src/routing/internal-router.ts +2 -0
- package/src/routing/pg-sync-router.ts +113 -0
- package/src/runtime.ts +20 -1
- package/src/scheduler.ts +26 -0
- package/src/server.ts +4 -0
- package/src/standalone-runtime.ts +16 -0
- package/src/utils/server-utils.ts +97 -1
- package/src/wake-registry.ts +27 -2
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
CREATE TABLE "pg_sync_bridges" (
|
|
2
|
+
"tenant_id" text DEFAULT 'default' NOT NULL,
|
|
3
|
+
"source_ref" text NOT NULL,
|
|
4
|
+
"options" jsonb NOT NULL,
|
|
5
|
+
"stream_url" text NOT NULL,
|
|
6
|
+
"shape_handle" text,
|
|
7
|
+
"shape_offset" text,
|
|
8
|
+
"initial_snapshot_complete" boolean DEFAULT false NOT NULL,
|
|
9
|
+
"last_touched_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
10
|
+
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
11
|
+
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
12
|
+
CONSTRAINT "pg_sync_bridges_tenant_id_source_ref_pk" PRIMARY KEY("tenant_id","source_ref"),
|
|
13
|
+
CONSTRAINT "uq_pg_sync_bridges_stream_url" UNIQUE("tenant_id","stream_url")
|
|
14
|
+
);
|
|
@@ -106,6 +106,13 @@
|
|
|
106
106
|
"when": 1780600000000,
|
|
107
107
|
"tag": "0014_entity_type_slash_commands",
|
|
108
108
|
"breakpoints": true
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
"idx": 15,
|
|
112
|
+
"version": "7",
|
|
113
|
+
"when": 1779728400000,
|
|
114
|
+
"tag": "0015_pg_sync_bridges",
|
|
115
|
+
"breakpoints": true
|
|
109
116
|
}
|
|
110
117
|
]
|
|
111
118
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@electric-ax/agents-server",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.20",
|
|
4
4
|
"description": "Electric Agents entity runtime server",
|
|
5
5
|
"author": "Durable Stream contributors",
|
|
6
6
|
"bin": {
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"@durable-streams/client": "^0.2.6",
|
|
40
40
|
"@durable-streams/server": "^0.3.7",
|
|
41
41
|
"@durable-streams/state": "^0.3.1",
|
|
42
|
-
"@electric-sql/client": "^1.5.
|
|
42
|
+
"@electric-sql/client": "^1.5.21",
|
|
43
43
|
"@mariozechner/pi-agent-core": "^0.70.2",
|
|
44
44
|
"@opentelemetry/api": "^1.9.1",
|
|
45
45
|
"@sinclair/typebox": "^0.34.48",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"pino-pretty": "^13.0.0",
|
|
55
55
|
"postgres": "^3.4.0",
|
|
56
56
|
"undici": "^7.24.7",
|
|
57
|
-
"@electric-ax/agents-runtime": "0.3.
|
|
57
|
+
"@electric-ax/agents-runtime": "0.3.13"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
60
|
"@types/node": "^22.19.15",
|
|
@@ -65,9 +65,9 @@
|
|
|
65
65
|
"tsx": "^4.19.0",
|
|
66
66
|
"typescript": "^5.0.0",
|
|
67
67
|
"vitest": "^4.1.0",
|
|
68
|
-
"@electric-ax/agents": "0.4.
|
|
69
|
-
"@electric-ax/agents-server-conformance-tests": "0.1.
|
|
70
|
-
"@electric-ax/agents-server-ui": "0.4.
|
|
68
|
+
"@electric-ax/agents": "0.4.17",
|
|
69
|
+
"@electric-ax/agents-server-conformance-tests": "0.1.12",
|
|
70
|
+
"@electric-ax/agents-server-ui": "0.4.20"
|
|
71
71
|
},
|
|
72
72
|
"files": [
|
|
73
73
|
"dist",
|
package/src/db/schema.ts
CHANGED
|
@@ -621,6 +621,34 @@ export const scheduledTasks = pgTable(
|
|
|
621
621
|
]
|
|
622
622
|
)
|
|
623
623
|
|
|
624
|
+
export const pgSyncBridges = pgTable(
|
|
625
|
+
`pg_sync_bridges`,
|
|
626
|
+
{
|
|
627
|
+
tenantId: text(`tenant_id`).notNull().default(`default`),
|
|
628
|
+
sourceRef: text(`source_ref`).notNull(),
|
|
629
|
+
options: jsonb(`options`).notNull(),
|
|
630
|
+
streamUrl: text(`stream_url`).notNull(),
|
|
631
|
+
shapeHandle: text(`shape_handle`),
|
|
632
|
+
shapeOffset: text(`shape_offset`),
|
|
633
|
+
initialSnapshotComplete: boolean(`initial_snapshot_complete`)
|
|
634
|
+
.notNull()
|
|
635
|
+
.default(false),
|
|
636
|
+
lastTouchedAt: timestamp(`last_touched_at`, { withTimezone: true })
|
|
637
|
+
.notNull()
|
|
638
|
+
.defaultNow(),
|
|
639
|
+
createdAt: timestamp(`created_at`, { withTimezone: true })
|
|
640
|
+
.notNull()
|
|
641
|
+
.defaultNow(),
|
|
642
|
+
updatedAt: timestamp(`updated_at`, { withTimezone: true })
|
|
643
|
+
.notNull()
|
|
644
|
+
.defaultNow(),
|
|
645
|
+
},
|
|
646
|
+
(table) => [
|
|
647
|
+
primaryKey({ columns: [table.tenantId, table.sourceRef] }),
|
|
648
|
+
unique(`uq_pg_sync_bridges_stream_url`).on(table.tenantId, table.streamUrl),
|
|
649
|
+
]
|
|
650
|
+
)
|
|
651
|
+
|
|
624
652
|
export const entityBridges = pgTable(
|
|
625
653
|
`entity_bridges`,
|
|
626
654
|
{
|
package/src/entity-manager.ts
CHANGED
|
@@ -336,6 +336,14 @@ function cloneRecord<T extends Record<string, unknown>>(value: T): T {
|
|
|
336
336
|
return JSON.parse(JSON.stringify(value)) as T
|
|
337
337
|
}
|
|
338
338
|
|
|
339
|
+
function withOptionalTxid<T>(
|
|
340
|
+
entity: T,
|
|
341
|
+
txid: number | undefined
|
|
342
|
+
): T & { txid?: number } {
|
|
343
|
+
if (txid === undefined) return entity as T & { txid?: number }
|
|
344
|
+
return { ...entity, txid }
|
|
345
|
+
}
|
|
346
|
+
|
|
339
347
|
/**
|
|
340
348
|
* Orchestrates the Electric Agents entity lifecycle: register types, spawn, send, kill.
|
|
341
349
|
*
|
|
@@ -2336,7 +2344,7 @@ export class EntityManager {
|
|
|
2336
2344
|
entityUrl: string,
|
|
2337
2345
|
req: SendRequest,
|
|
2338
2346
|
opts?: { producerId?: string }
|
|
2339
|
-
): Promise<
|
|
2347
|
+
): Promise<{ txid: string }> {
|
|
2340
2348
|
const entity = await this.validateSendRequest(entityUrl, req)
|
|
2341
2349
|
if (
|
|
2342
2350
|
this.isForkWorkLockedEntity(entityUrl) &&
|
|
@@ -2384,9 +2392,11 @@ export class EntityManager {
|
|
|
2384
2392
|
await this.entityBridgeManager?.onEntityChanged(entityUrl)
|
|
2385
2393
|
}
|
|
2386
2394
|
|
|
2395
|
+
const txid = crypto.randomUUID()
|
|
2387
2396
|
const envelope = entityStateSchema.inbox.insert({
|
|
2388
2397
|
key,
|
|
2389
2398
|
value,
|
|
2399
|
+
headers: { txid },
|
|
2390
2400
|
} as any)
|
|
2391
2401
|
|
|
2392
2402
|
const encoded = this.encodeChangeEvent(envelope as Record<string, unknown>)
|
|
@@ -2395,7 +2405,7 @@ export class EntityManager {
|
|
|
2395
2405
|
await this.streamClient.appendIdempotent(entity.streams.main, encoded, {
|
|
2396
2406
|
producerId: opts.producerId,
|
|
2397
2407
|
})
|
|
2398
|
-
return
|
|
2408
|
+
return { txid }
|
|
2399
2409
|
}
|
|
2400
2410
|
|
|
2401
2411
|
await this.streamClient.append(entity.streams.main, encoded)
|
|
@@ -2407,9 +2417,11 @@ export class EntityManager {
|
|
|
2407
2417
|
type: `identity`,
|
|
2408
2418
|
key: `self`,
|
|
2409
2419
|
value: identity,
|
|
2420
|
+
headers: { txid },
|
|
2410
2421
|
})
|
|
2411
2422
|
)
|
|
2412
2423
|
}
|
|
2424
|
+
return { txid }
|
|
2413
2425
|
} catch (err) {
|
|
2414
2426
|
if (this.isClosedStreamError(err)) {
|
|
2415
2427
|
throw new ElectricAgentsError(
|
|
@@ -2431,7 +2443,7 @@ export class EntityManager {
|
|
|
2431
2443
|
mode?: `immediate` | `queued` | `paused` | `steer`
|
|
2432
2444
|
status?: `pending` | `processed` | `cancelled`
|
|
2433
2445
|
}
|
|
2434
|
-
): Promise<
|
|
2446
|
+
): Promise<{ txid: string }> {
|
|
2435
2447
|
const entity = await this.registry.getEntity(entityUrl)
|
|
2436
2448
|
if (!entity) {
|
|
2437
2449
|
throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404)
|
|
@@ -2463,17 +2475,23 @@ export class EntityManager {
|
|
|
2463
2475
|
)
|
|
2464
2476
|
}
|
|
2465
2477
|
|
|
2478
|
+
const txid = crypto.randomUUID()
|
|
2466
2479
|
const envelope = entityStateSchema.inbox.update({
|
|
2467
2480
|
key,
|
|
2468
2481
|
value,
|
|
2482
|
+
headers: { txid },
|
|
2469
2483
|
} as any)
|
|
2470
2484
|
await this.streamClient.append(
|
|
2471
2485
|
entity.streams.main,
|
|
2472
2486
|
this.encodeChangeEvent(envelope as Record<string, unknown>)
|
|
2473
2487
|
)
|
|
2488
|
+
return { txid }
|
|
2474
2489
|
}
|
|
2475
2490
|
|
|
2476
|
-
async deleteInboxMessage(
|
|
2491
|
+
async deleteInboxMessage(
|
|
2492
|
+
entityUrl: string,
|
|
2493
|
+
key: string
|
|
2494
|
+
): Promise<{ txid: string }> {
|
|
2477
2495
|
const entity = await this.registry.getEntity(entityUrl)
|
|
2478
2496
|
if (!entity) {
|
|
2479
2497
|
throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404)
|
|
@@ -2486,11 +2504,16 @@ export class EntityManager {
|
|
|
2486
2504
|
)
|
|
2487
2505
|
}
|
|
2488
2506
|
|
|
2489
|
-
const
|
|
2507
|
+
const txid = crypto.randomUUID()
|
|
2508
|
+
const envelope = entityStateSchema.inbox.delete({
|
|
2509
|
+
key,
|
|
2510
|
+
headers: { txid },
|
|
2511
|
+
} as any)
|
|
2490
2512
|
await this.streamClient.append(
|
|
2491
2513
|
entity.streams.main,
|
|
2492
2514
|
this.encodeChangeEvent(envelope as Record<string, unknown>)
|
|
2493
2515
|
)
|
|
2516
|
+
return { txid }
|
|
2494
2517
|
}
|
|
2495
2518
|
|
|
2496
2519
|
// ==========================================================================
|
|
@@ -2686,21 +2709,12 @@ export class EntityManager {
|
|
|
2686
2709
|
async setTag(
|
|
2687
2710
|
entityUrl: string,
|
|
2688
2711
|
key: string,
|
|
2689
|
-
req: SetTagRequest
|
|
2690
|
-
|
|
2691
|
-
): Promise<ElectricAgentsEntity> {
|
|
2712
|
+
req: SetTagRequest
|
|
2713
|
+
): Promise<ElectricAgentsEntity & { txid?: number }> {
|
|
2692
2714
|
const entity = await this.registry.getEntity(entityUrl)
|
|
2693
2715
|
if (!entity) {
|
|
2694
2716
|
throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404)
|
|
2695
2717
|
}
|
|
2696
|
-
|
|
2697
|
-
if (!this.isValidWriteToken(entity, token)) {
|
|
2698
|
-
throw new ElectricAgentsError(
|
|
2699
|
-
ErrCodeUnauthorized,
|
|
2700
|
-
`Invalid write token`,
|
|
2701
|
-
401
|
|
2702
|
-
)
|
|
2703
|
-
}
|
|
2704
2718
|
if (rejectsNormalWrites(entity.status)) {
|
|
2705
2719
|
throw new ElectricAgentsError(
|
|
2706
2720
|
ErrCodeNotRunning,
|
|
@@ -2731,26 +2745,17 @@ export class EntityManager {
|
|
|
2731
2745
|
await this.entityBridgeManager.onEntityChanged(entityUrl)
|
|
2732
2746
|
}
|
|
2733
2747
|
|
|
2734
|
-
return updated
|
|
2748
|
+
return withOptionalTxid(updated, result.txid)
|
|
2735
2749
|
}
|
|
2736
2750
|
|
|
2737
2751
|
async deleteTag(
|
|
2738
2752
|
entityUrl: string,
|
|
2739
|
-
key: string
|
|
2740
|
-
|
|
2741
|
-
): Promise<ElectricAgentsEntity> {
|
|
2753
|
+
key: string
|
|
2754
|
+
): Promise<ElectricAgentsEntity & { txid?: number }> {
|
|
2742
2755
|
const entity = await this.registry.getEntity(entityUrl)
|
|
2743
2756
|
if (!entity) {
|
|
2744
2757
|
throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404)
|
|
2745
2758
|
}
|
|
2746
|
-
|
|
2747
|
-
if (!this.isValidWriteToken(entity, token)) {
|
|
2748
|
-
throw new ElectricAgentsError(
|
|
2749
|
-
ErrCodeUnauthorized,
|
|
2750
|
-
`Invalid write token`,
|
|
2751
|
-
401
|
|
2752
|
-
)
|
|
2753
|
-
}
|
|
2754
2759
|
if (rejectsNormalWrites(entity.status)) {
|
|
2755
2760
|
throw new ElectricAgentsError(
|
|
2756
2761
|
ErrCodeNotRunning,
|
|
@@ -2773,7 +2778,7 @@ export class EntityManager {
|
|
|
2773
2778
|
await this.entityBridgeManager.onEntityChanged(entityUrl)
|
|
2774
2779
|
}
|
|
2775
2780
|
|
|
2776
|
-
return updated
|
|
2781
|
+
return withOptionalTxid(updated, result.txid)
|
|
2777
2782
|
}
|
|
2778
2783
|
|
|
2779
2784
|
async ensureEntitiesMembershipStream(
|
package/src/entity-registry.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
entityLineage,
|
|
11
11
|
entityPermissionGrants,
|
|
12
12
|
entityTypes,
|
|
13
|
+
pgSyncBridges,
|
|
13
14
|
entityTypePermissionGrants,
|
|
14
15
|
runnerRuntimeDiagnostics,
|
|
15
16
|
runners,
|
|
@@ -43,7 +44,7 @@ import type {
|
|
|
43
44
|
EntityTypePermissionGrant,
|
|
44
45
|
PermissionSubjectKind,
|
|
45
46
|
} from './electric-agents-types.js'
|
|
46
|
-
import type { EntityTags } from '@electric-ax/agents-runtime'
|
|
47
|
+
import type { EntityTags, PgSyncOptions } from '@electric-ax/agents-runtime'
|
|
47
48
|
import type { Principal } from './principal.js'
|
|
48
49
|
|
|
49
50
|
export class EntityAlreadyExistsError extends Error {
|
|
@@ -73,6 +74,19 @@ export interface EntityBridgeRow {
|
|
|
73
74
|
updatedAt: Date
|
|
74
75
|
}
|
|
75
76
|
|
|
77
|
+
export interface PgSyncBridgeRow {
|
|
78
|
+
tenantId: string
|
|
79
|
+
sourceRef: string
|
|
80
|
+
options: PgSyncOptions
|
|
81
|
+
streamUrl: string
|
|
82
|
+
shapeHandle?: string
|
|
83
|
+
shapeOffset?: string
|
|
84
|
+
initialSnapshotComplete: boolean
|
|
85
|
+
lastTouchedAt: Date
|
|
86
|
+
createdAt: Date
|
|
87
|
+
updatedAt: Date
|
|
88
|
+
}
|
|
89
|
+
|
|
76
90
|
export interface TagStreamOutboxRow {
|
|
77
91
|
id: number
|
|
78
92
|
tenantId: string
|
|
@@ -623,6 +637,13 @@ export class PostgresRegistry {
|
|
|
623
637
|
)
|
|
624
638
|
}
|
|
625
639
|
|
|
640
|
+
private pgSyncBridgeWhere(sourceRef: string) {
|
|
641
|
+
return and(
|
|
642
|
+
eq(pgSyncBridges.tenantId, this.tenantId),
|
|
643
|
+
eq(pgSyncBridges.sourceRef, sourceRef)
|
|
644
|
+
)
|
|
645
|
+
}
|
|
646
|
+
|
|
626
647
|
async createEntityType(et: ElectricAgentsEntityType): Promise<void> {
|
|
627
648
|
await this.db
|
|
628
649
|
.insert(entityTypes)
|
|
@@ -1422,6 +1443,7 @@ export class PostgresRegistry {
|
|
|
1422
1443
|
entity: ElectricAgentsEntity | null
|
|
1423
1444
|
changed: boolean
|
|
1424
1445
|
op?: `insert` | `update` | `delete`
|
|
1446
|
+
txid?: number
|
|
1425
1447
|
}> {
|
|
1426
1448
|
return this.mutateEntityTags(url, (oldTags) => {
|
|
1427
1449
|
const previous = oldTags[key]
|
|
@@ -1440,7 +1462,11 @@ export class PostgresRegistry {
|
|
|
1440
1462
|
async removeEntityTag(
|
|
1441
1463
|
url: string,
|
|
1442
1464
|
key: string
|
|
1443
|
-
): Promise<{
|
|
1465
|
+
): Promise<{
|
|
1466
|
+
entity: ElectricAgentsEntity | null
|
|
1467
|
+
changed: boolean
|
|
1468
|
+
txid?: number
|
|
1469
|
+
}> {
|
|
1444
1470
|
return this.mutateEntityTags(url, (oldTags) => {
|
|
1445
1471
|
if (!(key in oldTags)) return null
|
|
1446
1472
|
const { [key]: _removed, ...remaining } = oldTags
|
|
@@ -1465,6 +1491,7 @@ export class PostgresRegistry {
|
|
|
1465
1491
|
entity: ElectricAgentsEntity | null
|
|
1466
1492
|
changed: boolean
|
|
1467
1493
|
op?: `insert` | `update`
|
|
1494
|
+
txid?: number
|
|
1468
1495
|
}> {
|
|
1469
1496
|
return await this.db.transaction(async (tx) => {
|
|
1470
1497
|
const [row] = await tx
|
|
@@ -1485,7 +1512,7 @@ export class PostgresRegistry {
|
|
|
1485
1512
|
|
|
1486
1513
|
const nextTags = normalizeTags(mutation.nextTags)
|
|
1487
1514
|
const updatedAt = Date.now()
|
|
1488
|
-
await tx
|
|
1515
|
+
const [updateResult] = await tx
|
|
1489
1516
|
.update(entities)
|
|
1490
1517
|
.set({
|
|
1491
1518
|
tags: nextTags,
|
|
@@ -1493,6 +1520,10 @@ export class PostgresRegistry {
|
|
|
1493
1520
|
updatedAt,
|
|
1494
1521
|
})
|
|
1495
1522
|
.where(this.entityWhere(url))
|
|
1523
|
+
.returning({
|
|
1524
|
+
txid: sql<string>`pg_current_xact_id()::xid::text`,
|
|
1525
|
+
})
|
|
1526
|
+
const txid = updateResult ? parseInt(updateResult.txid) : undefined
|
|
1496
1527
|
|
|
1497
1528
|
await tx.insert(tagStreamOutbox).values({
|
|
1498
1529
|
tenantId: this.tenantId,
|
|
@@ -1513,10 +1544,103 @@ export class PostgresRegistry {
|
|
|
1513
1544
|
entity,
|
|
1514
1545
|
changed: true,
|
|
1515
1546
|
...(op === `insert` || op === `update` ? { op } : {}),
|
|
1547
|
+
...(txid !== undefined ? { txid } : {}),
|
|
1516
1548
|
}
|
|
1517
1549
|
})
|
|
1518
1550
|
}
|
|
1519
1551
|
|
|
1552
|
+
async upsertPgSyncBridge(row: {
|
|
1553
|
+
sourceRef: string
|
|
1554
|
+
options: PgSyncOptions
|
|
1555
|
+
streamUrl: string
|
|
1556
|
+
}): Promise<PgSyncBridgeRow> {
|
|
1557
|
+
await this.db
|
|
1558
|
+
.insert(pgSyncBridges)
|
|
1559
|
+
.values({
|
|
1560
|
+
tenantId: this.tenantId,
|
|
1561
|
+
sourceRef: row.sourceRef,
|
|
1562
|
+
options: row.options,
|
|
1563
|
+
streamUrl: row.streamUrl,
|
|
1564
|
+
lastTouchedAt: new Date(),
|
|
1565
|
+
updatedAt: new Date(),
|
|
1566
|
+
})
|
|
1567
|
+
.onConflictDoUpdate({
|
|
1568
|
+
target: [pgSyncBridges.tenantId, pgSyncBridges.sourceRef],
|
|
1569
|
+
set: {
|
|
1570
|
+
options: row.options,
|
|
1571
|
+
streamUrl: row.streamUrl,
|
|
1572
|
+
initialSnapshotComplete: false,
|
|
1573
|
+
lastTouchedAt: new Date(),
|
|
1574
|
+
updatedAt: new Date(),
|
|
1575
|
+
},
|
|
1576
|
+
})
|
|
1577
|
+
|
|
1578
|
+
const existing = await this.getPgSyncBridge(row.sourceRef)
|
|
1579
|
+
if (!existing)
|
|
1580
|
+
throw new Error(`Failed to load pgSync bridge ${row.sourceRef}`)
|
|
1581
|
+
return existing
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
async getPgSyncBridge(sourceRef: string): Promise<PgSyncBridgeRow | null> {
|
|
1585
|
+
const rows = await this.db
|
|
1586
|
+
.select()
|
|
1587
|
+
.from(pgSyncBridges)
|
|
1588
|
+
.where(this.pgSyncBridgeWhere(sourceRef))
|
|
1589
|
+
.limit(1)
|
|
1590
|
+
return rows[0] ? this.rowToPgSyncBridge(rows[0]) : null
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
async listPgSyncBridges(
|
|
1594
|
+
tenantId: string | null = this.tenantId
|
|
1595
|
+
): Promise<Array<PgSyncBridgeRow>> {
|
|
1596
|
+
const rows =
|
|
1597
|
+
tenantId === null
|
|
1598
|
+
? await this.db.select().from(pgSyncBridges)
|
|
1599
|
+
: await this.db
|
|
1600
|
+
.select()
|
|
1601
|
+
.from(pgSyncBridges)
|
|
1602
|
+
.where(eq(pgSyncBridges.tenantId, tenantId))
|
|
1603
|
+
return rows.map((row) => this.rowToPgSyncBridge(row))
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
async touchPgSyncBridge(sourceRef: string): Promise<void> {
|
|
1607
|
+
await this.db
|
|
1608
|
+
.update(pgSyncBridges)
|
|
1609
|
+
.set({ lastTouchedAt: new Date(), updatedAt: new Date() })
|
|
1610
|
+
.where(this.pgSyncBridgeWhere(sourceRef))
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
async updatePgSyncBridgeCursor(
|
|
1614
|
+
sourceRef: string,
|
|
1615
|
+
shapeHandle: string,
|
|
1616
|
+
shapeOffset: string,
|
|
1617
|
+
initialSnapshotComplete?: boolean
|
|
1618
|
+
): Promise<void> {
|
|
1619
|
+
await this.db
|
|
1620
|
+
.update(pgSyncBridges)
|
|
1621
|
+
.set({
|
|
1622
|
+
shapeHandle,
|
|
1623
|
+
shapeOffset,
|
|
1624
|
+
...(initialSnapshotComplete !== undefined
|
|
1625
|
+
? { initialSnapshotComplete }
|
|
1626
|
+
: {}),
|
|
1627
|
+
updatedAt: new Date(),
|
|
1628
|
+
})
|
|
1629
|
+
.where(this.pgSyncBridgeWhere(sourceRef))
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
async clearPgSyncBridgeCursor(sourceRef: string): Promise<void> {
|
|
1633
|
+
await this.db
|
|
1634
|
+
.update(pgSyncBridges)
|
|
1635
|
+
.set({
|
|
1636
|
+
shapeHandle: null,
|
|
1637
|
+
shapeOffset: null,
|
|
1638
|
+
initialSnapshotComplete: false,
|
|
1639
|
+
updatedAt: new Date(),
|
|
1640
|
+
})
|
|
1641
|
+
.where(this.pgSyncBridgeWhere(sourceRef))
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1520
1644
|
async upsertEntityBridge(row: {
|
|
1521
1645
|
sourceRef: string
|
|
1522
1646
|
tags: EntityTags
|
|
@@ -1911,6 +2035,23 @@ export class PostgresRegistry {
|
|
|
1911
2035
|
}
|
|
1912
2036
|
}
|
|
1913
2037
|
|
|
2038
|
+
private rowToPgSyncBridge(
|
|
2039
|
+
row: typeof pgSyncBridges.$inferSelect
|
|
2040
|
+
): PgSyncBridgeRow {
|
|
2041
|
+
return {
|
|
2042
|
+
tenantId: row.tenantId,
|
|
2043
|
+
sourceRef: row.sourceRef,
|
|
2044
|
+
options: row.options as PgSyncOptions,
|
|
2045
|
+
streamUrl: row.streamUrl,
|
|
2046
|
+
shapeHandle: row.shapeHandle ?? undefined,
|
|
2047
|
+
shapeOffset: row.shapeOffset ?? undefined,
|
|
2048
|
+
initialSnapshotComplete: row.initialSnapshotComplete,
|
|
2049
|
+
lastTouchedAt: row.lastTouchedAt,
|
|
2050
|
+
createdAt: row.createdAt,
|
|
2051
|
+
updatedAt: row.updatedAt,
|
|
2052
|
+
}
|
|
2053
|
+
}
|
|
2054
|
+
|
|
1914
2055
|
private rowToEntityBridge(
|
|
1915
2056
|
row: typeof entityBridges.$inferSelect
|
|
1916
2057
|
): EntityBridgeRow {
|
|
@@ -10,6 +10,10 @@ export function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
10
10
|
return typeof value === `object` && value !== null && !Array.isArray(value)
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
function getPgSyncManifestStreamPath(sourceRef: string): string {
|
|
14
|
+
return `/_electric/pg-sync/${sourceRef}`
|
|
15
|
+
}
|
|
16
|
+
|
|
13
17
|
export function extractManifestSourceUrl(
|
|
14
18
|
manifest: Record<string, unknown> | undefined
|
|
15
19
|
): string | undefined {
|
|
@@ -57,6 +61,12 @@ export function extractManifestSourceUrl(
|
|
|
57
61
|
: undefined
|
|
58
62
|
}
|
|
59
63
|
|
|
64
|
+
if (manifest.sourceType === `pgSync`) {
|
|
65
|
+
return typeof manifest.sourceRef === `string`
|
|
66
|
+
? getPgSyncManifestStreamPath(manifest.sourceRef)
|
|
67
|
+
: undefined
|
|
68
|
+
}
|
|
69
|
+
|
|
60
70
|
if (manifest.sourceType === `webhook`) {
|
|
61
71
|
if (typeof config?.streamUrl === `string`) return config.streamUrl
|
|
62
72
|
if (typeof config?.endpointKey === `string`) {
|