@electric-ax/agents-server 0.4.15 → 0.4.16

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.
@@ -73,7 +73,6 @@ import type { EntityBridgeCoordinator } from './entity-bridge-manager.js'
73
73
  import type { Principal } from './principal.js'
74
74
 
75
75
  type SpawnPersistResult = [
76
- PromiseSettledResult<void>,
77
76
  PromiseSettledResult<void>,
78
77
  PromiseSettledResult<number>,
79
78
  ]
@@ -163,14 +162,14 @@ type ForkSubtreeOptions = {
163
162
  rootInstanceId?: string
164
163
  waitTimeoutMs?: number
165
164
  waitPollMs?: number
165
+ createdBy?: string
166
166
  /**
167
167
  * Optional anchor pointing at an event on the source root's `main` stream.
168
168
  * When set: only events at or before the pointer are kept on the root's
169
169
  * forked `main`, and the root's manifest is filtered so that descendants
170
170
  * spawned after the pointer are dropped from the fork (their now-orphan
171
171
  * subtrees are not forked). The pointer applies only to the root's
172
- * `main` stream — `error` and shared-state streams clone at HEAD
173
- * regardless.
172
+ * `main` stream; shared-state streams clone at HEAD regardless.
174
173
  */
175
174
  forkPointer?: EventPointer
176
175
  }
@@ -497,7 +496,10 @@ export class EntityManager {
497
496
 
498
497
  async ensurePrincipal(principal: Principal): Promise<ElectricAgentsEntity> {
499
498
  const existing = await this.registry.getEntity(principal.url)
500
- if (existing) return existing
499
+ if (existing) {
500
+ await this.ensureUserPrincipal(principal)
501
+ return existing
502
+ }
501
503
  await this.ensurePrincipalEntityType()
502
504
  try {
503
505
  const entity = await this.spawn(`principal`, {
@@ -522,6 +524,7 @@ export class EntityManager {
522
524
  },
523
525
  })
524
526
  )
527
+ await this.ensureUserPrincipal(principal)
525
528
  return entity
526
529
  } catch (error) {
527
530
  if (
@@ -529,12 +532,21 @@ export class EntityManager {
529
532
  error.code === ErrCodeDuplicateURL
530
533
  ) {
531
534
  const raced = await this.registry.getEntity(principal.url)
532
- if (raced) return raced
535
+ if (raced) {
536
+ await this.ensureUserPrincipal(principal)
537
+ return raced
538
+ }
533
539
  }
534
540
  throw error
535
541
  }
536
542
  }
537
543
 
544
+ private async ensureUserPrincipal(principal: Principal): Promise<void> {
545
+ if (principal.kind === `user`) {
546
+ await this.registry.ensureUserForPrincipal(principal)
547
+ }
548
+ }
549
+
538
550
  // ==========================================================================
539
551
  // Spawn
540
552
  // ==========================================================================
@@ -624,7 +636,6 @@ export class EntityManager {
624
636
  ? principalUrl(instanceId)
625
637
  : `/${typeName}/${instanceId}`
626
638
  const mainPath = `${entityURL}/main`
627
- const errorPath = `${entityURL}/error`
628
639
 
629
640
  const subscriptionId = `${typeName}-handler`
630
641
 
@@ -676,7 +687,6 @@ export class EntityManager {
676
687
  url: entityURL,
677
688
  streams: {
678
689
  main: mainPath,
679
- error: errorPath,
680
690
  },
681
691
  subscription_id: subscriptionId,
682
692
  dispatch_policy: dispatchPolicy,
@@ -745,8 +755,8 @@ export class EntityManager {
745
755
  const queueEnterT0 = performance.now()
746
756
  const queueWaiting = this.spawnPersistQueue.length()
747
757
  const queueRunning = this.spawnPersistQueue.running()
748
- const [mainStreamResult, errorStreamResult, entityResult] =
749
- await this.spawnPersistQueue.push(async () => {
758
+ const [mainStreamResult, entityResult] = await this.spawnPersistQueue.push(
759
+ async () => {
750
760
  // Create entity first so it's visible in the DB before stream
751
761
  // creation can trigger webhooks that look up the entity.
752
762
  let entityTxid: number
@@ -756,41 +766,34 @@ export class EntityManager {
756
766
  )
757
767
  } catch (err) {
758
768
  return [
759
- { status: `fulfilled`, value: undefined },
760
769
  { status: `fulfilled`, value: undefined },
761
770
  { status: `rejected`, reason: err },
762
771
  ] as SpawnPersistResult
763
772
  }
764
773
 
765
- const [mainStreamResult, errorStreamResult] = await Promise.allSettled([
774
+ const [mainStreamResult] = await Promise.allSettled([
766
775
  this.streamClient.create(mainPath, {
767
776
  contentType,
768
777
  body: initialBody,
769
778
  }),
770
- this.streamClient.create(errorPath, { contentType }),
771
779
  ])
772
780
 
773
781
  return [
774
782
  mainStreamResult,
775
- errorStreamResult,
776
783
  { status: `fulfilled`, value: entityTxid },
777
784
  ] as SpawnPersistResult
778
- })
785
+ }
786
+ )
779
787
  const parallelMs = +(performance.now() - queueEnterT0).toFixed(2)
780
788
 
781
789
  if (
782
790
  mainStreamResult.status === `rejected` ||
783
- errorStreamResult.status === `rejected` ||
784
791
  entityResult.status === `rejected`
785
792
  ) {
786
793
  const entityReason =
787
794
  entityResult.status === `rejected` ? entityResult.reason : null
788
795
  const streamReason =
789
- mainStreamResult.status === `rejected`
790
- ? mainStreamResult.reason
791
- : errorStreamResult.status === `rejected`
792
- ? errorStreamResult.reason
793
- : null
796
+ mainStreamResult.status === `rejected` ? mainStreamResult.reason : null
794
797
  const isDuplicate = entityReason instanceof EntityAlreadyExistsError
795
798
  const isStreamConflict =
796
799
  !!streamReason &&
@@ -805,9 +808,6 @@ export class EntityManager {
805
808
  if (mainStreamResult.status === `fulfilled`) {
806
809
  rollbacks.push(this.streamClient.delete(mainPath))
807
810
  }
808
- if (errorStreamResult.status === `fulfilled`) {
809
- rollbacks.push(this.streamClient.delete(errorPath))
810
- }
811
811
  if (entityResult.status === `fulfilled`) {
812
812
  rollbacks.push(this.registry.deleteEntity(entityURL))
813
813
  }
@@ -834,9 +834,7 @@ export class EntityManager {
834
834
  const failure =
835
835
  mainStreamResult.status === `rejected`
836
836
  ? mainStreamResult.reason
837
- : errorStreamResult.status === `rejected`
838
- ? errorStreamResult.reason
839
- : (entityResult as PromiseRejectedResult).reason
837
+ : (entityResult as PromiseRejectedResult).reason
840
838
  if (failure instanceof Error) throw failure
841
839
  throw new ElectricAgentsError(
842
840
  `SPAWN_FAILED`,
@@ -1045,7 +1043,8 @@ export class EntityManager {
1045
1043
  const entityPlans = this.buildForkEntityPlans(
1046
1044
  effectiveSubtree,
1047
1045
  entityUrlMap,
1048
- stringMap
1046
+ stringMap,
1047
+ opts.createdBy
1049
1048
  )
1050
1049
 
1051
1050
  this.addForkLocks(
@@ -1077,13 +1076,6 @@ export class EntityManager {
1077
1076
  : undefined
1078
1077
  )
1079
1078
  createdStreams.push(plan.fork.streams.main)
1080
- // `error` always clones at HEAD — no canonical mapping
1081
- // between main-offset and error-offset.
1082
- await this.streamClient.fork(
1083
- plan.fork.streams.error,
1084
- plan.source.streams.error
1085
- )
1086
- createdStreams.push(plan.fork.streams.error)
1087
1079
  }
1088
1080
 
1089
1081
  for (const [sourceId, forkId] of sharedStateIdMap) {
@@ -1711,7 +1703,6 @@ export class EntityManager {
1711
1703
  for (const [sourceUrl, forkUrl] of entityUrlMap) {
1712
1704
  stringMap.set(sourceUrl, forkUrl)
1713
1705
  stringMap.set(`${sourceUrl}/main`, `${forkUrl}/main`)
1714
- stringMap.set(`${sourceUrl}/error`, `${forkUrl}/error`)
1715
1706
  }
1716
1707
  for (const [sourceId, forkId] of sharedStateIdMap) {
1717
1708
  stringMap.set(sourceId, forkId)
@@ -1726,7 +1717,8 @@ export class EntityManager {
1726
1717
  private buildForkEntityPlans(
1727
1718
  entitiesToFork: Array<ElectricAgentsEntity>,
1728
1719
  entityUrlMap: Map<string, string>,
1729
- stringMap: Map<string, string>
1720
+ stringMap: Map<string, string>,
1721
+ createdBy?: string
1730
1722
  ): Array<ForkEntityPlan> {
1731
1723
  const now = Date.now()
1732
1724
  return entitiesToFork.map((source) => {
@@ -1750,12 +1742,12 @@ export class EntityManager {
1750
1742
  status: `idle`,
1751
1743
  streams: {
1752
1744
  main: `${forkUrl}/main`,
1753
- error: `${forkUrl}/error`,
1754
1745
  },
1755
1746
  subscription_id: `${type}-handler`,
1756
1747
  write_token: randomUUID(),
1757
1748
  spawn_args: spawnArgs,
1758
1749
  parent,
1750
+ created_by: createdBy ?? source.created_by,
1759
1751
  created_at: now,
1760
1752
  updated_at: now,
1761
1753
  }
@@ -2047,12 +2039,7 @@ export class EntityManager {
2047
2039
  manifests: Map<string, Record<string, unknown>>
2048
2040
  ): Promise<void> {
2049
2041
  for (const [manifestKey, manifest] of manifests) {
2050
- await this.syncEntitiesManifestSource(
2051
- entityUrl,
2052
- manifestKey,
2053
- `upsert`,
2054
- manifest
2055
- )
2042
+ await this.syncManifestLinks(entityUrl, manifestKey, `upsert`, manifest)
2056
2043
 
2057
2044
  const wake = buildManifestWakeRegistration(
2058
2045
  entityUrl,
@@ -2125,6 +2112,7 @@ export class EntityManager {
2125
2112
  {
2126
2113
  entityUrl: targetUrl,
2127
2114
  from: senderUrl,
2115
+ from_agent: senderUrl,
2128
2116
  payload: manifest.payload,
2129
2117
  key: `scheduled-${producerId}`,
2130
2118
  type:
@@ -2191,7 +2179,7 @@ export class EntityManager {
2191
2179
  `msg-in-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
2192
2180
 
2193
2181
  const value: Record<string, unknown> = {
2194
- from: req.from,
2182
+ from: req.from_principal ?? req.from,
2195
2183
  payload: req.payload,
2196
2184
  timestamp: now,
2197
2185
  mode: req.mode ?? `immediate`,
@@ -2200,6 +2188,12 @@ export class EntityManager {
2200
2188
  ? `pending`
2201
2189
  : `processed`,
2202
2190
  }
2191
+ if (req.from_principal) {
2192
+ value.from_principal = req.from_principal
2193
+ }
2194
+ if (req.from_agent) {
2195
+ value.from_agent = req.from_agent
2196
+ }
2203
2197
  if (req.type) {
2204
2198
  value.message_type = req.type
2205
2199
  }
@@ -2610,14 +2604,21 @@ export class EntityManager {
2610
2604
  return updated
2611
2605
  }
2612
2606
 
2613
- async ensureEntitiesMembershipStream(tags: Record<string, string>): Promise<{
2607
+ async ensureEntitiesMembershipStream(
2608
+ tags: Record<string, string>,
2609
+ principal: { url: string; kind: string }
2610
+ ): Promise<{
2614
2611
  sourceRef: string
2615
2612
  streamUrl: string
2616
2613
  }> {
2617
2614
  if (!this.entityBridgeManager) {
2618
2615
  throw new Error(`Entity bridge manager not configured`)
2619
2616
  }
2620
- return this.entityBridgeManager.register(this.validateTags(tags))
2617
+ return this.entityBridgeManager.register(
2618
+ this.validateTags(tags),
2619
+ principal.url,
2620
+ principal.kind
2621
+ )
2621
2622
  }
2622
2623
 
2623
2624
  async writeManifestEntry(
@@ -2650,12 +2651,12 @@ export class EntityManager {
2650
2651
  await this.streamClient.appendIdempotent(entity.streams.main, encoded, {
2651
2652
  producerId: opts.producerId,
2652
2653
  })
2653
- await this.syncEntitiesManifestSource(entityUrl, key, operation, value)
2654
+ await this.syncManifestLinks(entityUrl, key, operation, value)
2654
2655
  return
2655
2656
  }
2656
2657
 
2657
2658
  await this.streamClient.append(entity.streams.main, encoded)
2658
- await this.syncEntitiesManifestSource(entityUrl, key, operation, value)
2659
+ await this.syncManifestLinks(entityUrl, key, operation, value)
2659
2660
  }
2660
2661
 
2661
2662
  async upsertCronSchedule(
@@ -2950,6 +2951,8 @@ export class EntityManager {
2950
2951
  {
2951
2952
  entityUrl,
2952
2953
  from: req.from,
2954
+ from_principal: req.from_principal,
2955
+ from_agent: req.from_agent,
2953
2956
  payload: req.payload,
2954
2957
  key: req.key,
2955
2958
  type: req.type,
@@ -3031,7 +3034,7 @@ export class EntityManager {
3031
3034
  })
3032
3035
  }
3033
3036
 
3034
- private async syncEntitiesManifestSource(
3037
+ private async syncManifestLinks(
3035
3038
  entityUrl: string,
3036
3039
  manifestKey: string,
3037
3040
  operation: `insert` | `update` | `upsert` | `delete`,
@@ -3044,6 +3047,14 @@ export class EntityManager {
3044
3047
  manifestKey,
3045
3048
  sourceRef
3046
3049
  )
3050
+
3051
+ const sharedStateId =
3052
+ operation === `delete` ? undefined : this.extractSharedStateId(value)
3053
+ await this.registry.replaceSharedStateLink(
3054
+ entityUrl,
3055
+ manifestKey,
3056
+ sharedStateId
3057
+ )
3047
3058
  }
3048
3059
 
3049
3060
  private extractEntitiesSourceRef(
@@ -3059,6 +3070,24 @@ export class EntityManager {
3059
3070
  return undefined
3060
3071
  }
3061
3072
 
3073
+ private extractSharedStateId(
3074
+ manifest?: Record<string, unknown>
3075
+ ): string | undefined {
3076
+ if (manifest?.kind === `shared-state` && typeof manifest.id === `string`) {
3077
+ return manifest.id
3078
+ }
3079
+
3080
+ if (manifest?.kind !== `source` || manifest.sourceType !== `db`) {
3081
+ return undefined
3082
+ }
3083
+
3084
+ if (typeof manifest.sourceRef === `string`) {
3085
+ return manifest.sourceRef
3086
+ }
3087
+ const config = isRecord(manifest.config) ? manifest.config : undefined
3088
+ return typeof config?.id === `string` ? config.id : undefined
3089
+ }
3090
+
3062
3091
  /**
3063
3092
  * Read a child entity's stream and extract concatenated text deltas
3064
3093
  * for a specific run, plus any error messages for that run.
@@ -3334,19 +3363,8 @@ export class EntityManager {
3334
3363
  return
3335
3364
  }
3336
3365
 
3337
- const errorCloseEvent = {
3338
- type: `signal`,
3339
- key: signalEvent.key,
3340
- value: signalEvent.value,
3341
- headers: signalEvent.headers,
3342
- }
3343
- const errorSignalData = this.encodeChangeEvent(
3344
- errorCloseEvent as unknown as Record<string, unknown>
3345
- )
3346
-
3347
3366
  for (const [streamPath, data] of [
3348
3367
  [entity.streams.main, signalData],
3349
- [entity.streams.error, errorSignalData],
3350
3368
  ] as const) {
3351
3369
  try {
3352
3370
  await this.streamClient.append(streamPath, data, { close: true })
@@ -3,6 +3,7 @@ import {
3
3
  assertTags,
4
4
  buildTagsIndex,
5
5
  getEntitiesStreamPath,
6
+ hashString,
6
7
  normalizeTags,
7
8
  sourceRefForTags,
8
9
  } from '@electric-ax/agents-runtime'
@@ -15,6 +16,7 @@ import { PostgresRegistry } from './entity-registry.js'
15
16
  import { electricUrlWithPath } from './utils/electric-url.js'
16
17
  import { serverLog } from './utils/log.js'
17
18
  import { isUnregisteredTenantError } from './tenant.js'
19
+ import { isBuiltInSystemPrincipalUrl } from './principal.js'
18
20
  import type { DrizzleDB } from './db/index.js'
19
21
  import type { EntityBridgeCoordinator } from './entity-bridge-manager.js'
20
22
  import type { EntityBridgeRow } from './entity-registry.js'
@@ -37,6 +39,7 @@ interface EntityShapeRow extends Row<unknown> {
37
39
  type: string
38
40
  status: `spawning` | `running` | `idle` | `stopped`
39
41
  tags: EntityTags
42
+ created_by?: string | null
40
43
  spawn_args?: Record<string, unknown> | null
41
44
  sandbox?: { profile: string } | null
42
45
  parent?: string | null
@@ -53,6 +56,7 @@ const ENTITY_SHAPE_COLUMNS = [
53
56
  `type`,
54
57
  `status`,
55
58
  `tags`,
59
+ `created_by`,
56
60
  `spawn_args`,
57
61
  `sandbox`,
58
62
  `parent`,
@@ -89,6 +93,16 @@ function sourceRefFromStreamPath(streamPath: string): string | null {
89
93
  return match?.[1] ?? null
90
94
  }
91
95
 
96
+ function principalScopedSourceRef(
97
+ tagSourceRef: string,
98
+ principalUrl: string,
99
+ principalKind: string
100
+ ): string {
101
+ return `${tagSourceRef}-${hashString(
102
+ JSON.stringify({ principalKind, principalUrl })
103
+ )}`
104
+ }
105
+
92
106
  function sameMember(
93
107
  left: EntityMembershipRow | undefined,
94
108
  right: EntityMembershipRow
@@ -125,6 +139,9 @@ class ProjectedEntityBridge {
125
139
  readonly sourceRef: string
126
140
  readonly tags: EntityTags
127
141
  readonly streamUrl: string
142
+ private readonly principalUrl?: string
143
+ private readonly principalKind?: string
144
+ private readonly permissionBypass: boolean
128
145
 
129
146
  private currentMembers = new Map<string, EntityMembershipRow>()
130
147
  private producer: IdempotentProducer | null = null
@@ -132,12 +149,16 @@ class ProjectedEntityBridge {
132
149
 
133
150
  constructor(
134
151
  row: EntityBridgeRow,
152
+ private registry: PostgresRegistry,
135
153
  private streamClient: StreamClient
136
154
  ) {
137
155
  this.tenantId = row.tenantId
138
156
  this.sourceRef = row.sourceRef
139
157
  this.tags = normalizeTags(row.tags)
140
158
  this.streamUrl = row.streamUrl
159
+ this.principalUrl = row.principalUrl
160
+ this.principalKind = row.principalKind
161
+ this.permissionBypass = isBuiltInSystemPrincipalUrl(row.principalUrl)
141
162
  }
142
163
 
143
164
  async start(initialEntities: Iterable<EntityShapeRow>): Promise<void> {
@@ -159,7 +180,7 @@ class ProjectedEntityBridge {
159
180
  }
160
181
  )
161
182
  await this.loadCurrentMembers()
162
- this.reconcile(initialEntities)
183
+ await this.reconcile(initialEntities)
163
184
  }
164
185
 
165
186
  async stop(): Promise<void> {
@@ -175,13 +196,14 @@ class ProjectedEntityBridge {
175
196
  }
176
197
  }
177
198
 
178
- reconcile(entities: Iterable<EntityShapeRow>): void {
199
+ async reconcile(entities: Iterable<EntityShapeRow>): Promise<void> {
179
200
  if (this.stopped) return
180
201
 
181
202
  const staleMembers = new Map(this.currentMembers)
182
203
  for (const entity of entities) {
183
204
  if (entity.tenant_id !== this.tenantId) continue
184
205
  if (!entityMatchesTags(entity, this.tags)) continue
206
+ if (!(await this.canReadEntity(entity))) continue
185
207
  staleMembers.delete(entity.url)
186
208
  this.upsertEntity(entity)
187
209
  }
@@ -192,11 +214,14 @@ class ProjectedEntityBridge {
192
214
  }
193
215
  }
194
216
 
195
- applyEntity(entity: EntityShapeRow): void {
217
+ async applyEntity(entity: EntityShapeRow): Promise<void> {
196
218
  if (this.stopped) return
197
219
  if (entity.tenant_id !== this.tenantId) return
198
220
 
199
- if (!entityMatchesTags(entity, this.tags)) {
221
+ if (
222
+ !entityMatchesTags(entity, this.tags) ||
223
+ !(await this.canReadEntity(entity))
224
+ ) {
200
225
  const existing = this.currentMembers.get(entity.url)
201
226
  if (!existing) return
202
227
  this.append(`delete`, existing)
@@ -231,6 +256,16 @@ class ProjectedEntityBridge {
231
256
  }
232
257
  }
233
258
 
259
+ private async canReadEntity(entity: EntityShapeRow): Promise<boolean> {
260
+ if (this.permissionBypass) return true
261
+ if (!this.principalUrl || !this.principalKind) return false
262
+ if (entity.created_by === this.principalUrl) return true
263
+ return await this.registry.hasEntityPermission(entity.url, `read`, {
264
+ principalUrl: this.principalUrl,
265
+ principalKind: this.principalKind,
266
+ })
267
+ }
268
+
234
269
  private async ensureStream(): Promise<void> {
235
270
  if (!(await this.streamClient.exists(this.streamUrl))) {
236
271
  await this.streamClient.create(this.streamUrl, {
@@ -377,7 +412,9 @@ export class EntityProjector {
377
412
  async register(
378
413
  tenantId: string,
379
414
  registry: PostgresRegistry,
380
- tagsInput: unknown
415
+ tagsInput: unknown,
416
+ principalUrl: string,
417
+ principalKind: string
381
418
  ): Promise<{ sourceRef: string; streamUrl: string }> {
382
419
  if (!this.electricUrl) {
383
420
  throw new Error(
@@ -388,12 +425,18 @@ export class EntityProjector {
388
425
  await this.start()
389
426
  this.registries.set(tenantId, registry)
390
427
  const tags = normalizeTags(assertTags(tagsInput))
391
- const sourceRef = sourceRefForTags(tags)
428
+ const sourceRef = principalScopedSourceRef(
429
+ sourceRefForTags(tags),
430
+ principalUrl,
431
+ principalKind
432
+ )
392
433
  const streamUrl = getEntitiesStreamPath(sourceRef)
393
434
  const row = await registry.upsertEntityBridge({
394
435
  sourceRef,
395
436
  tags,
396
437
  streamUrl,
438
+ principalUrl,
439
+ principalKind,
397
440
  })
398
441
  await registry.touchEntityBridge(sourceRef)
399
442
  await this.ensureProjection(row)
@@ -436,8 +479,12 @@ export class EntityProjector {
436
479
  }
437
480
  }
438
481
 
439
- async onEntityChanged(_tenantId: string, _entityUrl: string): Promise<void> {
440
- // Membership updates come from the shared Electric entities shape.
482
+ async onEntityChanged(tenantId: string, entityUrl: string): Promise<void> {
483
+ const entity = this.entities.get(entityKey(tenantId, entityUrl))
484
+ if (!entity) return
485
+ for (const projection of this.projectionsForTenant(tenantId)) {
486
+ await projection.applyEntity(entity)
487
+ }
441
488
  }
442
489
 
443
490
  async loadTenantBridges(
@@ -523,18 +570,20 @@ export class EntityProjector {
523
570
  }
524
571
  if (message.headers.control === `up-to-date`) {
525
572
  this.upToDate = true
526
- this.reconcileAll()
573
+ await this.reconcileAll()
527
574
  this.readyResolve?.()
528
575
  }
529
576
  continue
530
577
  }
531
578
 
532
579
  if (!isChangeMessage(message)) continue
533
- this.applyChangeMessage(message)
580
+ await this.applyChangeMessage(message)
534
581
  }
535
582
  }
536
583
 
537
- private applyChangeMessage(message: ChangeMessage<EntityShapeRow>): void {
584
+ private async applyChangeMessage(
585
+ message: ChangeMessage<EntityShapeRow>
586
+ ): Promise<void> {
538
587
  const entity = message.value
539
588
  const key = entityKey(entity.tenant_id, entity.url)
540
589
  if (message.headers.operation === `delete`) {
@@ -550,7 +599,7 @@ export class EntityProjector {
550
599
  this.entities.set(key, entity)
551
600
  if (this.upToDate) {
552
601
  for (const projection of this.projectionsForTenant(entity.tenant_id)) {
553
- projection.applyEntity(entity)
602
+ await projection.applyEntity(entity)
554
603
  }
555
604
  }
556
605
  }
@@ -642,7 +691,11 @@ export class EntityProjector {
642
691
  }
643
692
  throw error
644
693
  }
645
- const projection = new ProjectedEntityBridge(row, streamClient)
694
+ const projection = new ProjectedEntityBridge(
695
+ row,
696
+ this.registryForTenant(row.tenantId),
697
+ streamClient
698
+ )
646
699
  await projection.start(this.entitiesForTenant(row.tenantId))
647
700
  this.projections.set(key, projection)
648
701
  })().finally(() => {
@@ -665,9 +718,9 @@ export class EntityProjector {
665
718
  )
666
719
  }
667
720
 
668
- private reconcileAll(): void {
721
+ private async reconcileAll(): Promise<void> {
669
722
  for (const projection of this.projections.values()) {
670
- projection.reconcile(this.entitiesForTenant(projection.tenantId))
723
+ await projection.reconcile(this.entitiesForTenant(projection.tenantId))
671
724
  }
672
725
  }
673
726
 
@@ -733,14 +786,20 @@ export class EntityProjectorTenantFacade implements EntityBridgeCoordinator {
733
786
 
734
787
  async stop(): Promise<void> {}
735
788
 
736
- async register(tagsInput: unknown): Promise<{
789
+ async register(
790
+ tagsInput: unknown,
791
+ principalUrl: string,
792
+ principalKind: string
793
+ ): Promise<{
737
794
  sourceRef: string
738
795
  streamUrl: string
739
796
  }> {
740
797
  return await this.projector.register(
741
798
  this.tenantId,
742
799
  this.registry,
743
- tagsInput
800
+ tagsInput,
801
+ principalUrl,
802
+ principalKind
744
803
  )
745
804
  }
746
805