@electric-ax/agents-server 0.4.19 → 0.5.0
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 +692 -45
- package/dist/index.cjs +678 -41
- package/dist/index.d.cts +2519 -2216
- package/dist/index.d.ts +2518 -2217
- package/dist/index.js +679 -42
- package/drizzle/0015_pg_sync_bridges.sql +14 -0
- package/drizzle/0016_entity_type_externally_writable_collections.sql +1 -0
- package/drizzle/meta/_journal.json +14 -0
- package/package.json +6 -6
- package/src/db/schema.ts +32 -0
- package/src/electric-agents-types.ts +23 -0
- package/src/entity-manager.ts +160 -29
- package/src/entity-registry.ts +158 -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 +89 -18
- package/src/routing/entity-types-router.ts +56 -0
- 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
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,
|
|
@@ -42,8 +43,9 @@ import type {
|
|
|
42
43
|
EntityTypePermission,
|
|
43
44
|
EntityTypePermissionGrant,
|
|
44
45
|
PermissionSubjectKind,
|
|
46
|
+
ExternallyWritableCollectionConfig,
|
|
45
47
|
} from './electric-agents-types.js'
|
|
46
|
-
import type { EntityTags } from '@electric-ax/agents-runtime'
|
|
48
|
+
import type { EntityTags, PgSyncOptions } from '@electric-ax/agents-runtime'
|
|
47
49
|
import type { Principal } from './principal.js'
|
|
48
50
|
|
|
49
51
|
export class EntityAlreadyExistsError extends Error {
|
|
@@ -73,6 +75,19 @@ export interface EntityBridgeRow {
|
|
|
73
75
|
updatedAt: Date
|
|
74
76
|
}
|
|
75
77
|
|
|
78
|
+
export interface PgSyncBridgeRow {
|
|
79
|
+
tenantId: string
|
|
80
|
+
sourceRef: string
|
|
81
|
+
options: PgSyncOptions
|
|
82
|
+
streamUrl: string
|
|
83
|
+
shapeHandle?: string
|
|
84
|
+
shapeOffset?: string
|
|
85
|
+
initialSnapshotComplete: boolean
|
|
86
|
+
lastTouchedAt: Date
|
|
87
|
+
createdAt: Date
|
|
88
|
+
updatedAt: Date
|
|
89
|
+
}
|
|
90
|
+
|
|
76
91
|
export interface TagStreamOutboxRow {
|
|
77
92
|
id: number
|
|
78
93
|
tenantId: string
|
|
@@ -623,6 +638,13 @@ export class PostgresRegistry {
|
|
|
623
638
|
)
|
|
624
639
|
}
|
|
625
640
|
|
|
641
|
+
private pgSyncBridgeWhere(sourceRef: string) {
|
|
642
|
+
return and(
|
|
643
|
+
eq(pgSyncBridges.tenantId, this.tenantId),
|
|
644
|
+
eq(pgSyncBridges.sourceRef, sourceRef)
|
|
645
|
+
)
|
|
646
|
+
}
|
|
647
|
+
|
|
626
648
|
async createEntityType(et: ElectricAgentsEntityType): Promise<void> {
|
|
627
649
|
await this.db
|
|
628
650
|
.insert(entityTypes)
|
|
@@ -633,6 +655,8 @@ export class PostgresRegistry {
|
|
|
633
655
|
creationSchema: et.creation_schema ?? null,
|
|
634
656
|
inboxSchemas: et.inbox_schemas ?? null,
|
|
635
657
|
stateSchemas: et.state_schemas ?? null,
|
|
658
|
+
externallyWritableCollections:
|
|
659
|
+
et.externally_writable_collections ?? null,
|
|
636
660
|
slashCommands: et.slash_commands ?? null,
|
|
637
661
|
serveEndpoint: et.serve_endpoint ?? null,
|
|
638
662
|
defaultDispatchPolicy: et.default_dispatch_policy ?? null,
|
|
@@ -647,6 +671,8 @@ export class PostgresRegistry {
|
|
|
647
671
|
creationSchema: et.creation_schema ?? null,
|
|
648
672
|
inboxSchemas: et.inbox_schemas ?? null,
|
|
649
673
|
stateSchemas: et.state_schemas ?? null,
|
|
674
|
+
externallyWritableCollections:
|
|
675
|
+
et.externally_writable_collections ?? null,
|
|
650
676
|
slashCommands: et.slash_commands ?? null,
|
|
651
677
|
serveEndpoint: et.serve_endpoint ?? null,
|
|
652
678
|
defaultDispatchPolicy: et.default_dispatch_policy ?? null,
|
|
@@ -670,6 +696,8 @@ export class PostgresRegistry {
|
|
|
670
696
|
creationSchema: et.creation_schema ?? null,
|
|
671
697
|
inboxSchemas: et.inbox_schemas ?? null,
|
|
672
698
|
stateSchemas: et.state_schemas ?? null,
|
|
699
|
+
externallyWritableCollections:
|
|
700
|
+
et.externally_writable_collections ?? null,
|
|
673
701
|
slashCommands: et.slash_commands ?? null,
|
|
674
702
|
serveEndpoint: et.serve_endpoint ?? null,
|
|
675
703
|
defaultDispatchPolicy: et.default_dispatch_policy ?? null,
|
|
@@ -712,6 +740,8 @@ export class PostgresRegistry {
|
|
|
712
740
|
creationSchema: et.creation_schema ?? null,
|
|
713
741
|
inboxSchemas: et.inbox_schemas ?? null,
|
|
714
742
|
stateSchemas: et.state_schemas ?? null,
|
|
743
|
+
externallyWritableCollections:
|
|
744
|
+
et.externally_writable_collections ?? null,
|
|
715
745
|
slashCommands: et.slash_commands ?? null,
|
|
716
746
|
serveEndpoint: et.serve_endpoint ?? null,
|
|
717
747
|
defaultDispatchPolicy: et.default_dispatch_policy ?? null,
|
|
@@ -1422,6 +1452,7 @@ export class PostgresRegistry {
|
|
|
1422
1452
|
entity: ElectricAgentsEntity | null
|
|
1423
1453
|
changed: boolean
|
|
1424
1454
|
op?: `insert` | `update` | `delete`
|
|
1455
|
+
txid?: number
|
|
1425
1456
|
}> {
|
|
1426
1457
|
return this.mutateEntityTags(url, (oldTags) => {
|
|
1427
1458
|
const previous = oldTags[key]
|
|
@@ -1440,7 +1471,11 @@ export class PostgresRegistry {
|
|
|
1440
1471
|
async removeEntityTag(
|
|
1441
1472
|
url: string,
|
|
1442
1473
|
key: string
|
|
1443
|
-
): Promise<{
|
|
1474
|
+
): Promise<{
|
|
1475
|
+
entity: ElectricAgentsEntity | null
|
|
1476
|
+
changed: boolean
|
|
1477
|
+
txid?: number
|
|
1478
|
+
}> {
|
|
1444
1479
|
return this.mutateEntityTags(url, (oldTags) => {
|
|
1445
1480
|
if (!(key in oldTags)) return null
|
|
1446
1481
|
const { [key]: _removed, ...remaining } = oldTags
|
|
@@ -1465,6 +1500,7 @@ export class PostgresRegistry {
|
|
|
1465
1500
|
entity: ElectricAgentsEntity | null
|
|
1466
1501
|
changed: boolean
|
|
1467
1502
|
op?: `insert` | `update`
|
|
1503
|
+
txid?: number
|
|
1468
1504
|
}> {
|
|
1469
1505
|
return await this.db.transaction(async (tx) => {
|
|
1470
1506
|
const [row] = await tx
|
|
@@ -1485,7 +1521,7 @@ export class PostgresRegistry {
|
|
|
1485
1521
|
|
|
1486
1522
|
const nextTags = normalizeTags(mutation.nextTags)
|
|
1487
1523
|
const updatedAt = Date.now()
|
|
1488
|
-
await tx
|
|
1524
|
+
const [updateResult] = await tx
|
|
1489
1525
|
.update(entities)
|
|
1490
1526
|
.set({
|
|
1491
1527
|
tags: nextTags,
|
|
@@ -1493,6 +1529,10 @@ export class PostgresRegistry {
|
|
|
1493
1529
|
updatedAt,
|
|
1494
1530
|
})
|
|
1495
1531
|
.where(this.entityWhere(url))
|
|
1532
|
+
.returning({
|
|
1533
|
+
txid: sql<string>`pg_current_xact_id()::xid::text`,
|
|
1534
|
+
})
|
|
1535
|
+
const txid = updateResult ? parseInt(updateResult.txid) : undefined
|
|
1496
1536
|
|
|
1497
1537
|
await tx.insert(tagStreamOutbox).values({
|
|
1498
1538
|
tenantId: this.tenantId,
|
|
@@ -1513,10 +1553,103 @@ export class PostgresRegistry {
|
|
|
1513
1553
|
entity,
|
|
1514
1554
|
changed: true,
|
|
1515
1555
|
...(op === `insert` || op === `update` ? { op } : {}),
|
|
1556
|
+
...(txid !== undefined ? { txid } : {}),
|
|
1516
1557
|
}
|
|
1517
1558
|
})
|
|
1518
1559
|
}
|
|
1519
1560
|
|
|
1561
|
+
async upsertPgSyncBridge(row: {
|
|
1562
|
+
sourceRef: string
|
|
1563
|
+
options: PgSyncOptions
|
|
1564
|
+
streamUrl: string
|
|
1565
|
+
}): Promise<PgSyncBridgeRow> {
|
|
1566
|
+
await this.db
|
|
1567
|
+
.insert(pgSyncBridges)
|
|
1568
|
+
.values({
|
|
1569
|
+
tenantId: this.tenantId,
|
|
1570
|
+
sourceRef: row.sourceRef,
|
|
1571
|
+
options: row.options,
|
|
1572
|
+
streamUrl: row.streamUrl,
|
|
1573
|
+
lastTouchedAt: new Date(),
|
|
1574
|
+
updatedAt: new Date(),
|
|
1575
|
+
})
|
|
1576
|
+
.onConflictDoUpdate({
|
|
1577
|
+
target: [pgSyncBridges.tenantId, pgSyncBridges.sourceRef],
|
|
1578
|
+
set: {
|
|
1579
|
+
options: row.options,
|
|
1580
|
+
streamUrl: row.streamUrl,
|
|
1581
|
+
initialSnapshotComplete: false,
|
|
1582
|
+
lastTouchedAt: new Date(),
|
|
1583
|
+
updatedAt: new Date(),
|
|
1584
|
+
},
|
|
1585
|
+
})
|
|
1586
|
+
|
|
1587
|
+
const existing = await this.getPgSyncBridge(row.sourceRef)
|
|
1588
|
+
if (!existing)
|
|
1589
|
+
throw new Error(`Failed to load pgSync bridge ${row.sourceRef}`)
|
|
1590
|
+
return existing
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
async getPgSyncBridge(sourceRef: string): Promise<PgSyncBridgeRow | null> {
|
|
1594
|
+
const rows = await this.db
|
|
1595
|
+
.select()
|
|
1596
|
+
.from(pgSyncBridges)
|
|
1597
|
+
.where(this.pgSyncBridgeWhere(sourceRef))
|
|
1598
|
+
.limit(1)
|
|
1599
|
+
return rows[0] ? this.rowToPgSyncBridge(rows[0]) : null
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
async listPgSyncBridges(
|
|
1603
|
+
tenantId: string | null = this.tenantId
|
|
1604
|
+
): Promise<Array<PgSyncBridgeRow>> {
|
|
1605
|
+
const rows =
|
|
1606
|
+
tenantId === null
|
|
1607
|
+
? await this.db.select().from(pgSyncBridges)
|
|
1608
|
+
: await this.db
|
|
1609
|
+
.select()
|
|
1610
|
+
.from(pgSyncBridges)
|
|
1611
|
+
.where(eq(pgSyncBridges.tenantId, tenantId))
|
|
1612
|
+
return rows.map((row) => this.rowToPgSyncBridge(row))
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
async touchPgSyncBridge(sourceRef: string): Promise<void> {
|
|
1616
|
+
await this.db
|
|
1617
|
+
.update(pgSyncBridges)
|
|
1618
|
+
.set({ lastTouchedAt: new Date(), updatedAt: new Date() })
|
|
1619
|
+
.where(this.pgSyncBridgeWhere(sourceRef))
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
async updatePgSyncBridgeCursor(
|
|
1623
|
+
sourceRef: string,
|
|
1624
|
+
shapeHandle: string,
|
|
1625
|
+
shapeOffset: string,
|
|
1626
|
+
initialSnapshotComplete?: boolean
|
|
1627
|
+
): Promise<void> {
|
|
1628
|
+
await this.db
|
|
1629
|
+
.update(pgSyncBridges)
|
|
1630
|
+
.set({
|
|
1631
|
+
shapeHandle,
|
|
1632
|
+
shapeOffset,
|
|
1633
|
+
...(initialSnapshotComplete !== undefined
|
|
1634
|
+
? { initialSnapshotComplete }
|
|
1635
|
+
: {}),
|
|
1636
|
+
updatedAt: new Date(),
|
|
1637
|
+
})
|
|
1638
|
+
.where(this.pgSyncBridgeWhere(sourceRef))
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
async clearPgSyncBridgeCursor(sourceRef: string): Promise<void> {
|
|
1642
|
+
await this.db
|
|
1643
|
+
.update(pgSyncBridges)
|
|
1644
|
+
.set({
|
|
1645
|
+
shapeHandle: null,
|
|
1646
|
+
shapeOffset: null,
|
|
1647
|
+
initialSnapshotComplete: false,
|
|
1648
|
+
updatedAt: new Date(),
|
|
1649
|
+
})
|
|
1650
|
+
.where(this.pgSyncBridgeWhere(sourceRef))
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1520
1653
|
async upsertEntityBridge(row: {
|
|
1521
1654
|
sourceRef: string
|
|
1522
1655
|
tags: EntityTags
|
|
@@ -1833,6 +1966,11 @@ export class PostgresRegistry {
|
|
|
1833
1966
|
state_schemas: row.stateSchemas as
|
|
1834
1967
|
| Record<string, Record<string, unknown>>
|
|
1835
1968
|
| undefined,
|
|
1969
|
+
externally_writable_collections:
|
|
1970
|
+
(row.externallyWritableCollections as Record<
|
|
1971
|
+
string,
|
|
1972
|
+
ExternallyWritableCollectionConfig
|
|
1973
|
+
> | null) ?? undefined,
|
|
1836
1974
|
slash_commands:
|
|
1837
1975
|
(row.slashCommands as ElectricAgentsEntityType[`slash_commands`]) ??
|
|
1838
1976
|
undefined,
|
|
@@ -1911,6 +2049,23 @@ export class PostgresRegistry {
|
|
|
1911
2049
|
}
|
|
1912
2050
|
}
|
|
1913
2051
|
|
|
2052
|
+
private rowToPgSyncBridge(
|
|
2053
|
+
row: typeof pgSyncBridges.$inferSelect
|
|
2054
|
+
): PgSyncBridgeRow {
|
|
2055
|
+
return {
|
|
2056
|
+
tenantId: row.tenantId,
|
|
2057
|
+
sourceRef: row.sourceRef,
|
|
2058
|
+
options: row.options as PgSyncOptions,
|
|
2059
|
+
streamUrl: row.streamUrl,
|
|
2060
|
+
shapeHandle: row.shapeHandle ?? undefined,
|
|
2061
|
+
shapeOffset: row.shapeOffset ?? undefined,
|
|
2062
|
+
initialSnapshotComplete: row.initialSnapshotComplete,
|
|
2063
|
+
lastTouchedAt: row.lastTouchedAt,
|
|
2064
|
+
createdAt: row.createdAt,
|
|
2065
|
+
updatedAt: row.updatedAt,
|
|
2066
|
+
}
|
|
2067
|
+
}
|
|
2068
|
+
|
|
1914
2069
|
private rowToEntityBridge(
|
|
1915
2070
|
row: typeof entityBridges.$inferSelect
|
|
1916
2071
|
): 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`) {
|