{"version":3,"file":"testing.mjs","names":[],"sources":["../../src/testing/test-builder.ts"],"sourcesContent":["//\n// Copyright 2026 DXOS.org\n//\n\nimport type * as SqlClient from '@effect/sql/SqlClient';\nimport * as Statement from '@effect/sql/Statement';\nimport * as Array from 'effect/Array';\nimport * as Effect from 'effect/Effect';\nimport * as Layer from 'effect/Layer';\nimport * as ManagedRuntime from 'effect/ManagedRuntime';\n\nimport { Context, Resource } from '@dxos/context';\nimport { RuntimeProvider } from '@dxos/effect';\nimport { type SpaceId } from '@dxos/keys';\nimport { FeedProtocol } from '@dxos/protocols';\nimport { SqlTransaction } from '@dxos/sql-sqlite';\nimport { layerMemory } from '@dxos/sql-sqlite/platform';\nimport * as SqlExport from '@dxos/sql-sqlite/SqlExport';\n\nimport { FeedStore } from '../feed-store';\nimport { SyncClient } from '../sync-client';\nimport { SyncServer } from '../sync-server';\n\ntype ProtocolMessage = FeedProtocol.ProtocolMessage;\nconst WellKnownNamespaces = FeedProtocol.WellKnownNamespaces;\ntype AppendRequest = FeedProtocol.AppendRequest;\ntype QueryRequest = FeedProtocol.QueryRequest;\n\nexport class TestBuilder extends Resource {\n  #peers: TestPeer[] = [];\n  readonly #spaceId: SpaceId;\n  readonly #feedNamespace: string;\n\n  constructor({\n    numPeers,\n    spaceId,\n    feedNamespace = WellKnownNamespaces.data,\n    logSql = false,\n  }: {\n    numPeers: number;\n    spaceId: SpaceId;\n    feedNamespace?: string;\n    logSql?: boolean;\n  }) {\n    super();\n    this.#spaceId = spaceId;\n    this.#feedNamespace = feedNamespace;\n    this.#peers = Array.makeBy(\n      numPeers,\n      (i) =>\n        new TestPeer({\n          isServer: i === 0,\n          actorId: `peer-${i}`,\n          serverPeerId: i === 0 ? undefined : 'peer-0',\n          sendMessage: (ctx, msg) => this.#routeMessage(ctx, msg),\n          logSql,\n        }),\n    );\n  }\n\n  get peers() {\n    return this.#peers;\n  }\n\n  get server() {\n    return this.#peers[0];\n  }\n\n  protected override async _open(): Promise<void> {\n    await Promise.all(this.#peers.map((peer) => peer.open()));\n  }\n\n  protected override async _close(): Promise<void> {\n    await Promise.all(this.#peers.map((peer) => peer.close()));\n  }\n\n  async pull(client: TestPeer, { limit = 10 }: { limit?: number } = {}): Promise<{ done: boolean }> {\n    return client.pull({ spaceId: this.#spaceId, feedNamespace: this.#feedNamespace, limit });\n  }\n\n  async push(client: TestPeer, { limit = 10 }: { limit?: number } = {}): Promise<{ done: boolean }> {\n    return client.push({ spaceId: this.#spaceId, feedNamespace: this.#feedNamespace, limit });\n  }\n\n  /** Route a message to the peer identified by recipientPeerId. Runs the recipient's handleMessage with that peer's runtime. */\n  #routeMessage(ctx: Context, msg: ProtocolMessage): Effect.Effect<void, unknown, never> {\n    const peer = this.#peers.find((p) => p.peerId === msg.recipientPeerId);\n    if (peer == null) {\n      return Effect.die(new Error(`TestPeer not found: ${msg.recipientPeerId}`));\n    }\n    const handleEffect =\n      peer.syncServer != null\n        ? peer.syncServer.handleMessage(ctx, msg)\n        : peer.syncClient != null\n          ? peer.syncClient.handleMessage(msg)\n          : null;\n    if (handleEffect == null) {\n      return Effect.die(new Error(`TestPeer has no handler: ${msg.recipientPeerId}`));\n    }\n    return Effect.promise(() => RuntimeProvider.runPromise(peer.runtime.runtimeEffect)(handleEffect));\n  }\n}\n\nconst loggingTransformer: Statement.Statement.Transformer = (stmt, _make, _, _span) =>\n  Effect.sync(() => {\n    const [sql, params] = stmt.compile();\n    console.log(sql.trim());\n    console.log(params);\n    return stmt;\n  });\n\nexport class TestPeer extends Resource {\n  readonly #peerId: string;\n  #feedStore: FeedStore;\n  #runtime: ManagedRuntime.ManagedRuntime<\n    SqlClient.SqlClient | SqlExport.SqlExport | SqlTransaction.SqlTransaction,\n    never\n  >;\n  #client?: SyncClient;\n  #server?: SyncServer;\n\n  constructor({\n    isServer,\n    actorId,\n    serverPeerId,\n    sendMessage,\n    logSql = false,\n  }: {\n    isServer: boolean;\n    actorId: string;\n    serverPeerId?: string;\n    sendMessage: (ctx: Context, msg: ProtocolMessage) => Effect.Effect<void, unknown, never>;\n    logSql?: boolean;\n  }) {\n    super();\n    this.#peerId = actorId;\n    this.#feedStore = new FeedStore({ localActorId: actorId, assignPositions: isServer });\n    const baseLayer = layerMemory.pipe(\n      Layer.provide(logSql ? Statement.setTransformer(loggingTransformer) : Layer.empty),\n    );\n    const transactionLayer = SqlTransaction.layer.pipe(Layer.provide(baseLayer));\n    this.#runtime = ManagedRuntime.make(Layer.merge(baseLayer, transactionLayer).pipe(Layer.orDie));\n    if (isServer) {\n      this.#server = new SyncServer({\n        peerId: actorId,\n        feedStore: this.#feedStore,\n        sendMessage,\n      });\n    } else {\n      this.#client = new SyncClient({\n        peerId: actorId,\n        serverPeerId: serverPeerId!,\n        feedStore: this.#feedStore,\n        sendMessage,\n      });\n    }\n  }\n\n  get peerId() {\n    return this.#peerId;\n  }\n\n  get feedStore() {\n    return this.#feedStore;\n  }\n\n  get runtime() {\n    return this.#runtime;\n  }\n\n  get syncClient() {\n    return this.#client;\n  }\n\n  get syncServer() {\n    return this.#server;\n  }\n\n  protected override async _open(): Promise<void> {\n    await this.#feedStore.migrate().pipe(RuntimeProvider.runPromise(this.#runtime.runtimeEffect));\n  }\n\n  protected override async _close(): Promise<void> {\n    await this.#runtime.dispose();\n  }\n\n  export(): Promise<Uint8Array> {\n    return Effect.gen(function* () {\n      const sql = yield* SqlExport.SqlExport;\n      return yield* sql.export;\n    }).pipe(RuntimeProvider.runPromise(this.#runtime.runtimeEffect));\n  }\n\n  getSyncState({ spaceId, feedNamespace }: { spaceId: SpaceId; feedNamespace: string }) {\n    return this.#feedStore\n      .getSyncState({ spaceId, feedNamespace })\n      .pipe(RuntimeProvider.runPromise(this.#runtime.runtimeEffect));\n  }\n\n  query(req: QueryRequest) {\n    return this.#feedStore.query(req).pipe(RuntimeProvider.runPromise(this.#runtime.runtimeEffect));\n  }\n\n  append(req: AppendRequest) {\n    return this.#feedStore.append(req).pipe(RuntimeProvider.runPromise(this.#runtime.runtimeEffect));\n  }\n\n  appendLocal(req: Parameters<FeedStore['appendLocal']>[0]) {\n    return this.#feedStore.appendLocal(req).pipe(RuntimeProvider.runPromise(this.#runtime.runtimeEffect));\n  }\n\n  pull({ spaceId, feedNamespace, limit = 10 }: { spaceId: SpaceId; feedNamespace: string; limit?: number }) {\n    return this.#client!\n      .pull(Context.default(), { spaceId, feedNamespace, limit })\n      .pipe(RuntimeProvider.runPromise(this.#runtime.runtimeEffect));\n  }\n\n  push({ spaceId, feedNamespace, limit = 10 }: { spaceId: SpaceId; feedNamespace: string; limit?: number }) {\n    return this.#client!\n      .push(Context.default(), { spaceId, feedNamespace, limit })\n      .pipe(RuntimeProvider.runPromise(this.#runtime.runtimeEffect));\n  }\n}\n"],"mappings":"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"}