@polyester/sdk 0.9.1 → 0.11.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.
Files changed (170) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/dist/catalogs/readers.d.ts.map +1 -1
  3. package/dist/catalogs/readers.js +22 -0
  4. package/dist/catalogs/readers.js.map +1 -1
  5. package/dist/core-client.d.ts.map +1 -1
  6. package/dist/core-client.js +49 -15
  7. package/dist/core-client.js.map +1 -1
  8. package/dist/environment.d.ts.map +1 -1
  9. package/dist/environment.js +3 -2
  10. package/dist/environment.js.map +1 -1
  11. package/dist/gen/auth/v1/api_keys_pb.d.ts +8 -7
  12. package/dist/gen/auth/v1/api_keys_pb.d.ts.map +1 -1
  13. package/dist/gen/auth/v1/api_keys_pb.js.map +1 -1
  14. package/dist/gen/auth/v1/policies_pb.d.ts +8 -4
  15. package/dist/gen/auth/v1/policies_pb.d.ts.map +1 -1
  16. package/dist/gen/auth/v1/policies_pb.js.map +1 -1
  17. package/dist/gen/auth/v1/subaccounts_pb.d.ts +382 -18
  18. package/dist/gen/auth/v1/subaccounts_pb.d.ts.map +1 -1
  19. package/dist/gen/auth/v1/subaccounts_pb.js +253 -38
  20. package/dist/gen/auth/v1/subaccounts_pb.js.map +1 -1
  21. package/dist/gen/orders/v1/orders_pb.d.ts +2 -2
  22. package/dist/gen/orders/v1/orders_pb.js.map +1 -1
  23. package/dist/gen/orders/v1/orders_read_pb.d.ts +8 -0
  24. package/dist/gen/orders/v1/orders_read_pb.d.ts.map +1 -1
  25. package/dist/gen/orders/v1/orders_read_pb.js +1 -1
  26. package/dist/gen/orders/v1/orders_read_pb.js.map +1 -1
  27. package/dist/gen/ratelimit/v1/ratelimit_pb.d.ts +2 -2
  28. package/dist/gen/ratelimit/v1/ratelimit_pb.js +1 -1
  29. package/dist/gen/ratelimit/v1/ratelimit_pb.js.map +1 -1
  30. package/dist/gen/triggers/v1/triggers_pb.d.ts +4 -4
  31. package/dist/gen/triggers/v1/triggers_pb.js.map +1 -1
  32. package/dist/index.d.ts +2 -2
  33. package/dist/realtime/client.d.ts.map +1 -1
  34. package/dist/realtime/client.js +13 -5
  35. package/dist/realtime/client.js.map +1 -1
  36. package/dist/realtime/ready-gated-subscription.js +68 -0
  37. package/dist/realtime/ready-gated-subscription.js.map +1 -0
  38. package/dist/realtime/snapshot-then-stream.js +19 -10
  39. package/dist/realtime/snapshot-then-stream.js.map +1 -1
  40. package/dist/server-client.d.ts +3 -1
  41. package/dist/server-client.d.ts.map +1 -1
  42. package/dist/server-client.js +9 -4
  43. package/dist/server-client.js.map +1 -1
  44. package/dist/services/auth/session.d.ts.map +1 -1
  45. package/dist/services/auth/session.js +4 -1
  46. package/dist/services/auth/session.js.map +1 -1
  47. package/dist/services/auth/token-storage.js +2 -2
  48. package/dist/services/auth/token-storage.js.map +1 -1
  49. package/dist/services/balances/balances.js +5 -8
  50. package/dist/services/balances/balances.js.map +1 -1
  51. package/dist/services/candles/candles.js +9 -12
  52. package/dist/services/candles/candles.js.map +1 -1
  53. package/dist/services/heatmap/heatmap.js +5 -8
  54. package/dist/services/heatmap/heatmap.js.map +1 -1
  55. package/dist/services/lifecycle/lifecycle.schemas.js +1 -1
  56. package/dist/services/market-data/market-data.d.ts.map +1 -1
  57. package/dist/services/market-data/market-data.js +5 -8
  58. package/dist/services/market-data/market-data.js.map +1 -1
  59. package/dist/services/market-data/market-data.schemas.d.ts.map +1 -1
  60. package/dist/services/market-data/market-data.schemas.js +3 -2
  61. package/dist/services/market-data/market-data.schemas.js.map +1 -1
  62. package/dist/services/market-overview/market-overview.d.ts.map +1 -1
  63. package/dist/services/market-overview/market-overview.js +7 -13
  64. package/dist/services/market-overview/market-overview.js.map +1 -1
  65. package/dist/services/orderbook/orderbook.d.ts.map +1 -1
  66. package/dist/services/orderbook/orderbook.js +18 -11
  67. package/dist/services/orderbook/orderbook.js.map +1 -1
  68. package/dist/services/orders/orders-batch.schemas.d.ts +6 -22
  69. package/dist/services/orders/orders-batch.schemas.d.ts.map +1 -1
  70. package/dist/services/orders/orders-batch.schemas.js +23 -20
  71. package/dist/services/orders/orders-batch.schemas.js.map +1 -1
  72. package/dist/services/orders/orders-identifiers.schemas.js +6 -1
  73. package/dist/services/orders/orders-identifiers.schemas.js.map +1 -1
  74. package/dist/services/orders/orders-input.schemas.d.ts +11 -43
  75. package/dist/services/orders/orders-input.schemas.d.ts.map +1 -1
  76. package/dist/services/orders/orders-input.schemas.js +10 -11
  77. package/dist/services/orders/orders-input.schemas.js.map +1 -1
  78. package/dist/services/orders/orders-modify.schemas.d.ts +1 -25
  79. package/dist/services/orders/orders-modify.schemas.d.ts.map +1 -1
  80. package/dist/services/orders/orders-modify.schemas.js +2 -4
  81. package/dist/services/orders/orders-modify.schemas.js.map +1 -1
  82. package/dist/services/orders/orders-output.schemas.d.ts +165 -27
  83. package/dist/services/orders/orders-output.schemas.d.ts.map +1 -1
  84. package/dist/services/orders/orders-output.schemas.js +1 -1
  85. package/dist/services/orders/orders-output.schemas.js.map +1 -1
  86. package/dist/services/orders/orders-risk.schemas.d.ts +1 -20
  87. package/dist/services/orders/orders-risk.schemas.d.ts.map +1 -1
  88. package/dist/services/orders/orders-risk.schemas.js +52 -15
  89. package/dist/services/orders/orders-risk.schemas.js.map +1 -1
  90. package/dist/services/orders/orders.codecs.js +12 -1
  91. package/dist/services/orders/orders.codecs.js.map +1 -1
  92. package/dist/services/orders/orders.d.ts.map +1 -1
  93. package/dist/services/orders/orders.js +6 -9
  94. package/dist/services/orders/orders.js.map +1 -1
  95. package/dist/services/rate-limits/rate-limits.schemas.d.ts +4 -4
  96. package/dist/services/rate-limits/rate-limits.schemas.js +1 -1
  97. package/dist/services/rate-limits/rate-limits.schemas.js.map +1 -1
  98. package/dist/services/subaccounts/index.d.ts +2 -2
  99. package/dist/services/subaccounts/subaccounts.codecs.js +29 -1
  100. package/dist/services/subaccounts/subaccounts.codecs.js.map +1 -1
  101. package/dist/services/subaccounts/subaccounts.d.ts +9 -1
  102. package/dist/services/subaccounts/subaccounts.d.ts.map +1 -1
  103. package/dist/services/subaccounts/subaccounts.js +19 -2
  104. package/dist/services/subaccounts/subaccounts.js.map +1 -1
  105. package/dist/services/subaccounts/subaccounts.schemas.d.ts +46 -6
  106. package/dist/services/subaccounts/subaccounts.schemas.d.ts.map +1 -1
  107. package/dist/services/subaccounts/subaccounts.schemas.js +28 -3
  108. package/dist/services/subaccounts/subaccounts.schemas.js.map +1 -1
  109. package/dist/services/subaccounts/subaccounts.types.d.ts +2 -2
  110. package/dist/services/trades/trades.d.ts +1 -1
  111. package/dist/services/trades/trades.js +6 -9
  112. package/dist/services/trades/trades.js.map +1 -1
  113. package/dist/services/trades/trades.schemas.d.ts +3 -0
  114. package/dist/services/trades/trades.schemas.d.ts.map +1 -1
  115. package/dist/services/trades/trades.schemas.js +2 -1
  116. package/dist/services/trades/trades.schemas.js.map +1 -1
  117. package/dist/services/trailing-oneof-inputs.js +3 -3
  118. package/dist/services/trailing-oneof-inputs.js.map +1 -1
  119. package/dist/services/transfers/index.d.ts +2 -2
  120. package/dist/services/transfers/transfers.d.ts +1 -2
  121. package/dist/services/transfers/transfers.d.ts.map +1 -1
  122. package/dist/services/transfers/transfers.js +8 -13
  123. package/dist/services/transfers/transfers.js.map +1 -1
  124. package/dist/services/transfers/transfers.schemas.d.ts +5 -5
  125. package/dist/services/transfers/transfers.schemas.d.ts.map +1 -1
  126. package/dist/services/transfers/transfers.schemas.js +31 -33
  127. package/dist/services/transfers/transfers.schemas.js.map +1 -1
  128. package/dist/services/triggers/trigger-input.schemas.d.ts +22 -9
  129. package/dist/services/triggers/trigger-input.schemas.d.ts.map +1 -1
  130. package/dist/services/triggers/trigger-input.schemas.js +20 -43
  131. package/dist/services/triggers/trigger-input.schemas.js.map +1 -1
  132. package/dist/services/triggers/triggers-output.schemas.d.ts +14 -14
  133. package/dist/services/triggers/triggers-output.schemas.js +5 -5
  134. package/dist/services/triggers/triggers-output.schemas.js.map +1 -1
  135. package/dist/services/triggers/triggers.d.ts.map +1 -1
  136. package/dist/services/triggers/triggers.js +10 -23
  137. package/dist/services/triggers/triggers.js.map +1 -1
  138. package/dist/services/zipper/zipper.d.ts.map +1 -1
  139. package/dist/services/zipper/zipper.js +15 -18
  140. package/dist/services/zipper/zipper.js.map +1 -1
  141. package/dist/shared/connect-error-mapping.d.ts.map +1 -1
  142. package/dist/shared/connect-error-mapping.js +9 -2
  143. package/dist/shared/connect-error-mapping.js.map +1 -1
  144. package/dist/shared/decimal-surface.d.ts.map +1 -1
  145. package/dist/shared/decimal-surface.js +40 -17
  146. package/dist/shared/decimal-surface.js.map +1 -1
  147. package/dist/shared/transports.d.ts.map +1 -1
  148. package/dist/shared/transports.js +33 -11
  149. package/dist/shared/transports.js.map +1 -1
  150. package/dist/utils/base58-id.js +1 -2
  151. package/dist/utils/base58-id.js.map +1 -1
  152. package/dist/utils/errors.d.ts.map +1 -1
  153. package/dist/utils/errors.js +4 -3
  154. package/dist/utils/errors.js.map +1 -1
  155. package/dist/utils/event-emitter.d.ts.map +1 -1
  156. package/dist/utils/event-emitter.js +6 -3
  157. package/dist/utils/event-emitter.js.map +1 -1
  158. package/dist/utils/jwt.d.ts.map +1 -1
  159. package/dist/utils/jwt.js +2 -2
  160. package/dist/utils/jwt.js.map +1 -1
  161. package/dist/utils/numbers.js +20 -3
  162. package/dist/utils/numbers.js.map +1 -1
  163. package/dist/utils/timestamp.js +27 -16
  164. package/dist/utils/timestamp.js.map +1 -1
  165. package/dist/utils/u128.js +6 -18
  166. package/dist/utils/u128.js.map +1 -1
  167. package/dist/wired-services.d.ts.map +1 -1
  168. package/dist/wired-services.js +2 -1
  169. package/dist/wired-services.js.map +1 -1
  170. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"client.js","names":["#config","#getAuthHeaders","#sharedSubs","#channelKind","#emitSubscriptionError","#publicClient","#connectionHandlers","#privateClient","#emitConnectionTokenError","#createPublicClient","#hasAuth","#createPrivateClient","#assertChannelAuth","#ensurePrivateClient","#ensurePublicClient","#attachSubscriptionNow","#attachSubscriptionWhenLoaded","#ensureClientForChannel","#subscriptionOpts","#pendingTeardowns","#attachSubscription","#callErrorHandler","#disconnectClientIfIdle","#hasChannelsForKind","#teardownSubscription","#getOrCreateSubscription","#callConsumerHandler","#disconnect","#disconnectPrivate"],"sources":["../../src/realtime/client.ts"],"sourcesContent":["import { fromBinary, type DescMessage, type MessageShape } from \"@bufbuild/protobuf\";\nimport type {\n Centrifuge,\n SubscriptionErrorContext,\n PublicationContext,\n Subscription,\n} from \"centrifuge/build/protobuf\";\nimport type * as CentrifugeModule from \"centrifuge/build/protobuf\";\nimport {\n createSdkSubscriptionErrorContext,\n fromCentrifugeSubscriptionError,\n type SdkSubscriptionErrorContext,\n} from \"../shared/subscription-errors.js\";\nimport { AuthenticationError, errorFromHttpStatus, InternalServerError } from \"../shared/errors.js\";\nimport { makeFetch } from \"../shared/transports.js\";\nimport { decodeProtoFrame } from \"../utils/streams.js\";\nimport type { ConnectChannelParams, PolyesterRealtime, SubscribeHandlers } from \"./types.js\";\n\nconst realtimeFetch = makeFetch();\n\ntype CentrifugeCtor = typeof CentrifugeModule.Centrifuge;\n\n// Centrifuge's protobuf build embeds the protobuf.js runtime (~300 KB minified).\n// Loading it lazily keeps it out of the eager module graph on both the server\n// (Cloudflare isolate cold start — SSR never opens a websocket) and the client\n// app shell; it is only fetched when the first subscription attaches.\nlet centrifugeCtorPromise: Promise<CentrifugeCtor> | null = null;\nlet loadedCentrifugeCtor: CentrifugeCtor | null = null;\nexport function __setRealtimeCentrifugeForTests(Centrifuge: CentrifugeCtor | null): void {\n loadedCentrifugeCtor = Centrifuge;\n centrifugeCtorPromise = Centrifuge ? Promise.resolve(Centrifuge) : null;\n}\n\nfunction loadCentrifuge(): Promise<CentrifugeCtor> {\n if ((import.meta as { env?: { SSR?: boolean } }).env?.SSR) {\n return Promise.reject(new Error(\"Realtime subscriptions are browser-only during SSR.\"));\n }\n\n centrifugeCtorPromise ??= import(\"centrifuge/build/protobuf\").then((mod) => {\n loadedCentrifugeCtor = mod.Centrifuge;\n return mod.Centrifuge;\n });\n return centrifugeCtorPromise;\n}\n\nexport interface RealtimeAuthRequest {\n url: string | URL;\n method: string;\n}\n\nexport interface RealtimeConfig {\n wsUrl: string;\n tokenEndpoint: string;\n subscribeEndpoint: string;\n getAuthHeaders?: (request: RealtimeAuthRequest) => Promise<HeadersInit> | HeadersInit;\n hasAuth?: () => boolean;\n}\n\ntype ResolvedRealtimeConfig = Pick<\n RealtimeConfig,\n \"wsUrl\" | \"tokenEndpoint\" | \"subscribeEndpoint\"\n> & {\n getAuthHeaders: (request: RealtimeAuthRequest) => Promise<HeadersInit> | HeadersInit;\n hasAuth: () => boolean;\n};\n\nexport type { ConnectChannelParams, PolyesterRealtime, SubscribeHandlers } from \"./types.js\";\n\ntype ConnectionHandler = { onConnected?: () => void; onDisconnected?: () => void };\ntype PublicationHandler<T = unknown> = (data: T) => void;\ntype ErrorHandler = (ctx: SdkSubscriptionErrorContext) => void;\ntype RealtimeClientKind = \"public\" | \"private\";\n\ninterface SharedSubscription {\n channel: string;\n sub: Subscription | null;\n client: Centrifuge | null;\n attachmentEpoch: number;\n consumers: number;\n subscriptionEpoch: number;\n publicationHandlers: Set<PublicationHandler>;\n subscribedHandlers: Set<(epoch: number) => void>;\n unsubscribedHandlers: Set<() => void>;\n errorHandlers: Set<ErrorHandler>;\n}\n\n/**\n * Shared Centrifuge realtime client that multiplexes public and private protobuf subscriptions across SDK services.\n */\nexport class RealtimeClient implements PolyesterRealtime {\n #publicClient: Centrifuge | null = null;\n #privateClient: Centrifuge | null = null;\n #connectionHandlers = new Set<ConnectionHandler>();\n #sharedSubs = new Map<string, SharedSubscription>();\n #pendingTeardowns = new Map<string, SharedSubscription>();\n readonly #config: ResolvedRealtimeConfig;\n\n constructor(config: RealtimeConfig) {\n this.#config = {\n wsUrl: config.wsUrl,\n tokenEndpoint: config.tokenEndpoint,\n subscribeEndpoint: config.subscribeEndpoint,\n getAuthHeaders: config.getAuthHeaders ?? (() => ({})),\n hasAuth: config.hasAuth ?? (() => false),\n };\n }\n\n async #getAuthHeaders(request: RealtimeAuthRequest): Promise<HeadersInit> {\n return this.#config.getAuthHeaders(request);\n }\n\n #emitSubscriptionError(shared: SharedSubscription, type: string, error: unknown): void {\n const ctx = createSdkSubscriptionErrorContext(shared.channel, type, error);\n for (const handler of shared.errorHandlers) {\n handler(ctx);\n }\n }\n\n #emitConnectionTokenError(error: unknown): void {\n for (const shared of this.#sharedSubs.values()) {\n if (this.#channelKind(shared.channel) === \"private\") {\n this.#emitSubscriptionError(shared, \"connection_token\", error);\n }\n }\n }\n\n #subscriptionOpts(shared: SharedSubscription, attachmentEpoch: number) {\n if (this.#channelKind(shared.channel) !== \"private\") return undefined;\n return {\n getToken: async () => {\n try {\n const url = new URL(this.#config.subscribeEndpoint);\n url.searchParams.set(\"channel\", shared.channel);\n const headers = await this.#getAuthHeaders({ url, method: \"GET\" });\n const res = await realtimeFetch(url, { headers });\n if (!res.ok) {\n throw errorFromHttpStatus(\n res.status,\n `Failed to fetch subscription token: ${res.status}`,\n );\n }\n const json = (await res.json()) as { token?: string };\n if (!json?.token) {\n throw new InternalServerError(\"Subscription token response had no token\");\n }\n return json.token;\n } catch (error) {\n if (shared.attachmentEpoch === attachmentEpoch) {\n this.#emitSubscriptionError(shared, \"subscription_token\", error);\n }\n throw error;\n }\n },\n };\n }\n\n #hasAuth(): boolean {\n return this.#config.hasAuth();\n }\n\n #channelKind(channel: string): RealtimeClientKind {\n return channel.startsWith(\"private:\") ? \"private\" : \"public\";\n }\n\n #createPublicClient(Centrifuge: CentrifugeCtor): Centrifuge {\n const client = new Centrifuge(this.#config.wsUrl);\n this.#publicClient = client;\n\n client.on(\"connected\", () => {\n if (this.#publicClient !== client) return;\n for (const h of this.#connectionHandlers) h.onConnected?.();\n });\n client.on(\"disconnected\", () => {\n if (this.#publicClient !== client) return;\n for (const h of this.#connectionHandlers) h.onDisconnected?.();\n });\n\n client.connect();\n return client;\n }\n\n #createPrivateClient(Centrifuge: CentrifugeCtor): Centrifuge {\n let client: Centrifuge;\n const opts = {\n getToken: async () => {\n try {\n const headers = await this.#getAuthHeaders({\n url: this.#config.tokenEndpoint,\n method: \"GET\",\n });\n const res = await realtimeFetch(this.#config.tokenEndpoint, {\n headers,\n });\n if (!res.ok) {\n throw errorFromHttpStatus(\n res.status,\n `Failed to fetch connection token: ${res.status}`,\n );\n }\n const json = (await res.json()) as { token?: string };\n if (!json?.token) {\n throw new InternalServerError(\"Connection token response had no token\");\n }\n return json.token;\n } catch (error) {\n if (this.#privateClient === client) {\n this.#emitConnectionTokenError(error);\n }\n throw error;\n }\n },\n };\n\n client = new Centrifuge(this.#config.wsUrl, opts);\n this.#privateClient = client;\n\n client.on(\"connected\", () => {\n if (this.#privateClient !== client) return;\n for (const h of this.#connectionHandlers) h.onConnected?.();\n });\n client.on(\"disconnected\", () => {\n if (this.#privateClient !== client) return;\n for (const h of this.#connectionHandlers) h.onDisconnected?.();\n });\n\n client.connect();\n return client;\n }\n\n #ensurePublicClient(Centrifuge: CentrifugeCtor): Centrifuge {\n return this.#publicClient ?? this.#createPublicClient(Centrifuge);\n }\n\n #ensurePrivateClient(Centrifuge: CentrifugeCtor): Centrifuge {\n if (!this.#hasAuth()) {\n throw new AuthenticationError(\n \"Cannot create authenticated realtime client without authentication\",\n );\n }\n return this.#privateClient ?? this.#createPrivateClient(Centrifuge);\n }\n\n #assertChannelAuth(channel: string): void {\n if (this.#channelKind(channel) === \"private\" && !this.#hasAuth()) {\n throw new AuthenticationError(\n `Cannot subscribe to private channel \"${channel}\" without authentication`,\n );\n }\n }\n\n #ensureClientForChannel(Centrifuge: CentrifugeCtor, channel: string): Centrifuge {\n this.#assertChannelAuth(channel);\n if (this.#channelKind(channel) === \"private\") {\n return this.#ensurePrivateClient(Centrifuge);\n }\n\n return this.#ensurePublicClient(Centrifuge);\n }\n\n #attachSubscription(shared: SharedSubscription): void {\n if (shared.sub) return;\n\n // Auth failures must surface synchronously to subscribe() callers, as\n // they did when centrifuge was imported statically.\n this.#assertChannelAuth(shared.channel);\n\n const attachmentEpoch = shared.attachmentEpoch + 1;\n shared.attachmentEpoch = attachmentEpoch;\n\n // Once the transport module is loaded, attachment stays fully\n // synchronous — only the very first attach pays the dynamic import.\n if (loadedCentrifugeCtor) {\n this.#attachSubscriptionNow(loadedCentrifugeCtor, shared, attachmentEpoch);\n return;\n }\n void this.#attachSubscriptionWhenLoaded(shared, attachmentEpoch);\n }\n\n async #attachSubscriptionWhenLoaded(\n shared: SharedSubscription,\n attachmentEpoch: number,\n ): Promise<void> {\n let Centrifuge: CentrifugeCtor;\n try {\n Centrifuge = await loadCentrifuge();\n } catch (error) {\n if (shared.attachmentEpoch !== attachmentEpoch) return;\n if (this.#sharedSubs.get(shared.channel) === shared) {\n this.#sharedSubs.delete(shared.channel);\n }\n this.#emitSubscriptionError(shared, \"transport_load\", error);\n return;\n }\n\n // The subscription may have been torn down or re-attached while the\n // transport module was loading.\n if (shared.attachmentEpoch !== attachmentEpoch || shared.sub) return;\n if (this.#sharedSubs.get(shared.channel) !== shared) return;\n\n try {\n this.#attachSubscriptionNow(Centrifuge, shared, attachmentEpoch);\n } catch (error) {\n this.#sharedSubs.delete(shared.channel);\n this.#emitSubscriptionError(shared, \"auth\", error);\n }\n }\n\n #attachSubscriptionNow(\n Centrifuge: CentrifugeCtor,\n shared: SharedSubscription,\n attachmentEpoch: number,\n ): void {\n const client = this.#ensureClientForChannel(Centrifuge, shared.channel);\n\n const sub = client.newSubscription(\n shared.channel,\n this.#subscriptionOpts(shared, attachmentEpoch),\n );\n shared.sub = sub;\n shared.client = client;\n\n sub.on(\"publication\", (ctx: PublicationContext) => {\n if (shared.sub !== sub || shared.attachmentEpoch !== attachmentEpoch) return;\n for (const handler of shared.publicationHandlers) handler(ctx.data);\n });\n sub.on(\"subscribed\", () => {\n if (shared.sub !== sub || shared.attachmentEpoch !== attachmentEpoch) return;\n const subscriptionEpoch = ++shared.subscriptionEpoch;\n for (const handler of shared.subscribedHandlers) handler(subscriptionEpoch);\n });\n sub.on(\"unsubscribed\", () => {\n if (shared.sub !== sub || shared.attachmentEpoch !== attachmentEpoch) return;\n for (const handler of shared.unsubscribedHandlers) handler();\n });\n sub.on(\"error\", (ctx: SubscriptionErrorContext) => {\n if (shared.sub !== sub || shared.attachmentEpoch !== attachmentEpoch) return;\n const sdkCtx = fromCentrifugeSubscriptionError(ctx);\n for (const handler of shared.errorHandlers) handler(sdkCtx);\n });\n\n sub.subscribe();\n }\n\n #getOrCreateSubscription(channel: string): SharedSubscription {\n const existing = this.#sharedSubs.get(channel);\n if (existing) return existing;\n\n const pending = this.#pendingTeardowns.get(channel);\n if (pending) {\n this.#pendingTeardowns.delete(channel);\n this.#sharedSubs.set(channel, pending);\n if (!pending.sub) {\n this.#attachSubscription(pending);\n } else if (pending.sub.state !== \"subscribed\") {\n pending.sub.subscribe();\n }\n return pending;\n }\n\n const shared: SharedSubscription = {\n channel,\n sub: null,\n client: null,\n attachmentEpoch: 0,\n consumers: 0,\n subscriptionEpoch: 0,\n publicationHandlers: new Set(),\n subscribedHandlers: new Set(),\n unsubscribedHandlers: new Set(),\n errorHandlers: new Set(),\n };\n\n this.#sharedSubs.set(channel, shared);\n try {\n this.#attachSubscription(shared);\n } catch (error) {\n this.#sharedSubs.delete(channel);\n throw error;\n }\n return shared;\n }\n\n #callErrorHandler(handler: ErrorHandler | undefined, ctx: SdkSubscriptionErrorContext): void {\n if (!handler) return;\n\n try {\n handler(ctx);\n } catch {\n // Keep error reporting isolated from other subscription consumers.\n }\n }\n\n #callConsumerHandler(\n channel: string,\n type: string,\n handler: () => void,\n onError?: ErrorHandler,\n ): void {\n try {\n handler();\n } catch (error) {\n this.#callErrorHandler(\n onError,\n createSdkSubscriptionErrorContext(channel, type, error),\n );\n }\n }\n\n #teardownSubscription(shared: SharedSubscription): void {\n const sub = shared.sub;\n const client = shared.client;\n const kind = this.#channelKind(shared.channel);\n shared.sub = null;\n shared.client = null;\n shared.attachmentEpoch++;\n if (!sub) return;\n\n try {\n if (sub.state !== \"unsubscribed\") {\n sub.unsubscribe();\n }\n } catch {\n // noop\n }\n try {\n client?.removeSubscription?.(sub);\n } catch {\n // noop\n }\n this.#disconnectClientIfIdle(kind);\n }\n\n #hasChannelsForKind(kind: RealtimeClientKind): boolean {\n for (const shared of this.#sharedSubs.values()) {\n if (this.#channelKind(shared.channel) === kind) return true;\n }\n for (const shared of this.#pendingTeardowns.values()) {\n if (this.#channelKind(shared.channel) === kind) return true;\n }\n return false;\n }\n\n #disconnectClientIfIdle(kind: RealtimeClientKind): void {\n if (this.#hasChannelsForKind(kind)) return;\n\n const client = kind === \"private\" ? this.#privateClient : this.#publicClient;\n if (kind === \"private\") {\n this.#privateClient = null;\n } else {\n this.#publicClient = null;\n }\n\n try {\n client?.disconnect();\n } catch {\n // noop\n }\n }\n\n #disconnect(): void {\n const publicClient = this.#publicClient;\n const privateClient = this.#privateClient;\n\n this.#pendingTeardowns.clear();\n this.#sharedSubs.clear();\n this.#publicClient = null;\n this.#privateClient = null;\n\n try {\n publicClient?.disconnect();\n } catch {\n // noop\n }\n try {\n privateClient?.disconnect();\n } catch {\n // noop\n }\n this.#connectionHandlers.clear();\n }\n\n #disconnectPrivate(): void {\n const privateClient = this.#privateClient;\n this.#privateClient = null;\n\n for (const [channel, shared] of this.#sharedSubs) {\n if (this.#channelKind(channel) !== \"private\") continue;\n this.#sharedSubs.delete(channel);\n this.#teardownSubscription(shared);\n }\n for (const [channel, shared] of this.#pendingTeardowns) {\n if (this.#channelKind(channel) !== \"private\") continue;\n this.#pendingTeardowns.delete(channel);\n this.#teardownSubscription(shared);\n }\n\n try {\n privateClient?.disconnect();\n } catch {\n // noop\n }\n }\n\n /**\n * Subscribes to a realtime channel and returns an unsubscribe function. Missing\n * authentication is reported to `onError`, or thrown synchronously when no error\n * observer is provided.\n */\n subscribe<T>(channel: string, handlers: SubscribeHandlers<T>): () => void {\n let shared: SharedSubscription;\n try {\n shared = this.#getOrCreateSubscription(channel);\n } catch (error) {\n if (!(error instanceof AuthenticationError)) throw error;\n if (!handlers.onError) throw error;\n\n const ctx = createSdkSubscriptionErrorContext(channel, \"auth\", error);\n queueMicrotask(() => this.#callErrorHandler(handlers.onError, ctx));\n return () => {};\n }\n shared.consumers++;\n\n const publicationHandler = handlers.onPublication;\n const errorHandler = handlers.onError;\n const subscribedHandler = handlers.onSubscribed;\n const unsubscribedHandler = handlers.onUnsubscribed;\n\n const onError: ErrorHandler | undefined = errorHandler\n ? (ctx) => this.#callErrorHandler(errorHandler, ctx)\n : undefined;\n const onPub: PublicationHandler = (data) => {\n this.#callConsumerHandler(\n channel,\n \"publication_handler\",\n () => publicationHandler(data as T),\n onError,\n );\n };\n let lastSubscribedEpoch = -1;\n const onSubscribed = subscribedHandler\n ? (subscriptionEpoch: number) => {\n if (subscriptionEpoch <= lastSubscribedEpoch) return;\n lastSubscribedEpoch = subscriptionEpoch;\n this.#callConsumerHandler(\n channel,\n \"subscribed_handler\",\n subscribedHandler,\n onError,\n );\n }\n : undefined;\n const onUnsubscribed = unsubscribedHandler\n ? () =>\n this.#callConsumerHandler(\n channel,\n \"unsubscribed_handler\",\n unsubscribedHandler,\n onError,\n )\n : undefined;\n\n shared.publicationHandlers.add(onPub);\n\n if (onSubscribed) shared.subscribedHandlers.add(onSubscribed);\n if (onUnsubscribed) shared.unsubscribedHandlers.add(onUnsubscribed);\n if (onError) shared.errorHandlers.add(onError);\n\n let closed = false;\n if (onSubscribed && shared.sub?.state === \"subscribed\") {\n const subscriptionEpoch = shared.subscriptionEpoch;\n queueMicrotask(() => {\n if (closed || shared.sub?.state !== \"subscribed\") return;\n onSubscribed(subscriptionEpoch);\n });\n }\n\n return () => {\n if (closed) return;\n closed = true;\n\n shared.publicationHandlers.delete(onPub);\n if (onSubscribed) shared.subscribedHandlers.delete(onSubscribed);\n if (onUnsubscribed) shared.unsubscribedHandlers.delete(onUnsubscribed);\n if (onError) shared.errorHandlers.delete(onError);\n\n shared.consumers--;\n if (shared.consumers <= 0) {\n const currentShared = this.#sharedSubs.get(channel);\n if (currentShared !== shared) return;\n\n this.#sharedSubs.delete(channel);\n this.#pendingTeardowns.set(channel, shared);\n\n queueMicrotask(() => {\n if (this.#pendingTeardowns.get(channel) !== shared) return;\n this.#pendingTeardowns.delete(channel);\n\n this.#teardownSubscription(shared);\n\n if (this.#sharedSubs.size === 0 && this.#pendingTeardowns.size === 0) {\n this.#disconnect();\n }\n });\n }\n };\n }\n\n /**\n * Connects to a realtime channel with a custom message decoder.\n */\n connectChannel<T extends DescMessage>(params: ConnectChannelParams<T>): () => void {\n return this.subscribe<Uint8Array>(params.channel, {\n onPublication: (data) => {\n let decoded: MessageShape<T>;\n try {\n decoded = fromBinary(params.schema, data);\n } catch (error) {\n this.#callErrorHandler(\n params.onError,\n createSdkSubscriptionErrorContext(params.channel, \"decode\", error),\n );\n return;\n }\n params.onPublication(decoded);\n },\n onSubscribed: params.onConnected,\n onUnsubscribed: params.onDisconnected,\n onError: params.onError,\n });\n }\n\n /**\n * Connects to a realtime channel that emits protobuf messages.\n */\n connectProtoChannel<T extends DescMessage>(params: ConnectChannelParams<T>): () => void {\n return this.subscribe<Uint8Array | MessageShape<T>>(params.channel, {\n onPublication: (data) => {\n let msg: MessageShape<T> | null;\n try {\n msg = decodeProtoFrame(params.schema, data);\n } catch (error) {\n this.#callErrorHandler(\n params.onError,\n createSdkSubscriptionErrorContext(params.channel, \"decode\", error),\n );\n return;\n }\n if (!msg) {\n this.#callErrorHandler(\n params.onError,\n createSdkSubscriptionErrorContext(\n params.channel,\n \"decode\",\n \"Unable to decode protobuf frame\",\n ),\n );\n return;\n }\n params.onPublication(msg);\n },\n onSubscribed: params.onConnected,\n onUnsubscribed: params.onDisconnected,\n onError: params.onError,\n });\n }\n\n /**\n * Disconnects the realtime client from all active channels.\n */\n disconnect(): void {\n this.#disconnect();\n }\n\n /**\n * Disconnects authenticated realtime state without touching public channels.\n */\n disconnectPrivate(): void {\n this.#disconnectPrivate();\n }\n\n get isConnected(): boolean {\n return this.#publicClient !== null || this.#privateClient !== null;\n }\n\n get activeChannels(): number {\n return this.#sharedSubs.size;\n }\n\n get totalConsumers(): number {\n let total = 0;\n for (const shared of this.#sharedSubs.values()) {\n total += shared.consumers;\n }\n return total;\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"client.js","names":["#config","#getAuthHeaders","#sharedSubs","#channelKind","#emitSubscriptionError","#publicClient","#connectionHandlers","#privateClient","#emitConnectionTokenError","#createPublicClient","#hasAuth","#createPrivateClient","#assertChannelAuth","#ensurePrivateClient","#ensurePublicClient","#attachSubscriptionNow","#attachSubscriptionWhenLoaded","#ensureClientForChannel","#subscriptionOpts","#pendingTeardowns","#attachSubscription","#callErrorHandler","#disconnectClientIfIdle","#hasChannelsForKind","#teardownSubscription","#getOrCreateSubscription","#callConsumerHandler","#disconnect","#disconnectPrivate"],"sources":["../../src/realtime/client.ts"],"sourcesContent":["import { fromBinary, type DescMessage, type MessageShape } from \"@bufbuild/protobuf\";\nimport type {\n Centrifuge,\n SubscriptionErrorContext,\n PublicationContext,\n Subscription,\n} from \"centrifuge/build/protobuf\";\nimport type * as CentrifugeModule from \"centrifuge/build/protobuf\";\nimport {\n createSdkSubscriptionErrorContext,\n fromCentrifugeSubscriptionError,\n type SdkSubscriptionErrorContext,\n} from \"../shared/subscription-errors.js\";\nimport { AuthenticationError, errorFromHttpStatus, InternalServerError } from \"../shared/errors.js\";\nimport { makeFetch } from \"../shared/transports.js\";\nimport { decodeProtoFrame } from \"../utils/streams.js\";\nimport type { ConnectChannelParams, PolyesterRealtime, SubscribeHandlers } from \"./types.js\";\n\nconst realtimeFetch = makeFetch();\n\ntype CentrifugeCtor = typeof CentrifugeModule.Centrifuge;\ntype CentrifugeModuleLoader = () => Promise<typeof CentrifugeModule>;\n\nconst defaultCentrifugeModuleLoader: CentrifugeModuleLoader = () =>\n import(\"centrifuge/build/protobuf\");\n\n// Centrifuge's protobuf build embeds the protobuf.js runtime (~300 KB minified).\n// Loading it lazily keeps it out of the eager module graph on both the server\n// (Cloudflare isolate cold start — SSR never opens a websocket) and the client\n// app shell; it is only fetched when the first subscription attaches.\nlet centrifugeCtorPromise: Promise<CentrifugeCtor> | null = null;\nlet loadedCentrifugeCtor: CentrifugeCtor | null = null;\nlet centrifugeModuleLoader = defaultCentrifugeModuleLoader;\nexport function __setRealtimeCentrifugeForTests(Centrifuge: CentrifugeCtor | null): void {\n loadedCentrifugeCtor = Centrifuge;\n centrifugeCtorPromise = Centrifuge ? Promise.resolve(Centrifuge) : null;\n}\n\n/** Replaces the lazy Centrifuge module loader for isolated transport-load tests. */\nexport function __setRealtimeCentrifugeLoaderForTests(loader: CentrifugeModuleLoader | null): void {\n centrifugeModuleLoader = loader ?? defaultCentrifugeModuleLoader;\n loadedCentrifugeCtor = null;\n centrifugeCtorPromise = null;\n}\n\nfunction loadCentrifuge(): Promise<CentrifugeCtor> {\n if (\n centrifugeModuleLoader === defaultCentrifugeModuleLoader &&\n (import.meta as { env?: { SSR?: boolean } }).env?.SSR\n ) {\n return Promise.reject(new Error(\"Realtime subscriptions are browser-only during SSR.\"));\n }\n\n if (!centrifugeCtorPromise) {\n const loadAttempt = Promise.resolve().then(centrifugeModuleLoader);\n const retryableLoad = loadAttempt.then(\n (mod) => {\n loadedCentrifugeCtor = mod.Centrifuge;\n return mod.Centrifuge;\n },\n (error) => {\n if (centrifugeCtorPromise === retryableLoad) centrifugeCtorPromise = null;\n throw error;\n },\n );\n centrifugeCtorPromise = retryableLoad;\n }\n return centrifugeCtorPromise;\n}\n\nexport interface RealtimeAuthRequest {\n url: string | URL;\n method: string;\n}\n\nexport interface RealtimeConfig {\n wsUrl: string;\n tokenEndpoint: string;\n subscribeEndpoint: string;\n getAuthHeaders?: (request: RealtimeAuthRequest) => Promise<HeadersInit> | HeadersInit;\n hasAuth?: () => boolean;\n}\n\ntype ResolvedRealtimeConfig = Pick<\n RealtimeConfig,\n \"wsUrl\" | \"tokenEndpoint\" | \"subscribeEndpoint\"\n> & {\n getAuthHeaders: (request: RealtimeAuthRequest) => Promise<HeadersInit> | HeadersInit;\n hasAuth: () => boolean;\n};\n\nexport type { ConnectChannelParams, PolyesterRealtime, SubscribeHandlers } from \"./types.js\";\n\ntype ConnectionHandler = { onConnected?: () => void; onDisconnected?: () => void };\ntype PublicationHandler<T = unknown> = (data: T) => void;\ntype ErrorHandler = (ctx: SdkSubscriptionErrorContext) => void;\ntype RealtimeClientKind = \"public\" | \"private\";\n\ninterface SharedSubscription {\n channel: string;\n sub: Subscription | null;\n client: Centrifuge | null;\n attachmentEpoch: number;\n consumers: number;\n subscriptionEpoch: number;\n publicationHandlers: Set<PublicationHandler>;\n subscribedHandlers: Set<(epoch: number) => void>;\n unsubscribedHandlers: Set<() => void>;\n errorHandlers: Set<ErrorHandler>;\n}\n\n/**\n * Shared Centrifuge realtime client that multiplexes public and private protobuf subscriptions across SDK services.\n */\nexport class RealtimeClient implements PolyesterRealtime {\n #publicClient: Centrifuge | null = null;\n #privateClient: Centrifuge | null = null;\n #connectionHandlers = new Set<ConnectionHandler>();\n #sharedSubs = new Map<string, SharedSubscription>();\n #pendingTeardowns = new Map<string, SharedSubscription>();\n readonly #config: ResolvedRealtimeConfig;\n\n constructor(config: RealtimeConfig) {\n this.#config = {\n wsUrl: config.wsUrl,\n tokenEndpoint: config.tokenEndpoint,\n subscribeEndpoint: config.subscribeEndpoint,\n getAuthHeaders: config.getAuthHeaders ?? (() => ({})),\n hasAuth: config.hasAuth ?? (() => false),\n };\n }\n\n async #getAuthHeaders(request: RealtimeAuthRequest): Promise<HeadersInit> {\n return this.#config.getAuthHeaders(request);\n }\n\n #emitSubscriptionError(shared: SharedSubscription, type: string, error: unknown): void {\n const ctx = createSdkSubscriptionErrorContext(shared.channel, type, error);\n for (const handler of shared.errorHandlers) {\n handler(ctx);\n }\n }\n\n #emitConnectionTokenError(error: unknown): void {\n for (const shared of this.#sharedSubs.values()) {\n if (this.#channelKind(shared.channel) === \"private\") {\n this.#emitSubscriptionError(shared, \"connection_token\", error);\n }\n }\n }\n\n #subscriptionOpts(shared: SharedSubscription, attachmentEpoch: number) {\n if (this.#channelKind(shared.channel) !== \"private\") return undefined;\n return {\n getToken: async () => {\n try {\n const url = new URL(this.#config.subscribeEndpoint);\n url.searchParams.set(\"channel\", shared.channel);\n const headers = await this.#getAuthHeaders({ url, method: \"GET\" });\n const res = await realtimeFetch(url, { headers });\n if (!res.ok) {\n throw errorFromHttpStatus(\n res.status,\n `Failed to fetch subscription token: ${res.status}`,\n );\n }\n const json = (await res.json()) as { token?: string };\n if (!json?.token) {\n throw new InternalServerError(\"Subscription token response had no token\");\n }\n return json.token;\n } catch (error) {\n if (shared.attachmentEpoch === attachmentEpoch) {\n this.#emitSubscriptionError(shared, \"subscription_token\", error);\n }\n throw error;\n }\n },\n };\n }\n\n #hasAuth(): boolean {\n return this.#config.hasAuth();\n }\n\n #channelKind(channel: string): RealtimeClientKind {\n return channel.startsWith(\"private:\") ? \"private\" : \"public\";\n }\n\n #createPublicClient(Centrifuge: CentrifugeCtor): Centrifuge {\n const client = new Centrifuge(this.#config.wsUrl);\n this.#publicClient = client;\n\n client.on(\"connected\", () => {\n if (this.#publicClient !== client) return;\n for (const h of this.#connectionHandlers) h.onConnected?.();\n });\n client.on(\"disconnected\", () => {\n if (this.#publicClient !== client) return;\n for (const h of this.#connectionHandlers) h.onDisconnected?.();\n });\n\n client.connect();\n return client;\n }\n\n #createPrivateClient(Centrifuge: CentrifugeCtor): Centrifuge {\n let client: Centrifuge;\n const opts = {\n getToken: async () => {\n try {\n const headers = await this.#getAuthHeaders({\n url: this.#config.tokenEndpoint,\n method: \"GET\",\n });\n const res = await realtimeFetch(this.#config.tokenEndpoint, {\n headers,\n });\n if (!res.ok) {\n throw errorFromHttpStatus(\n res.status,\n `Failed to fetch connection token: ${res.status}`,\n );\n }\n const json = (await res.json()) as { token?: string };\n if (!json?.token) {\n throw new InternalServerError(\"Connection token response had no token\");\n }\n return json.token;\n } catch (error) {\n if (this.#privateClient === client) {\n this.#emitConnectionTokenError(error);\n }\n throw error;\n }\n },\n };\n\n client = new Centrifuge(this.#config.wsUrl, opts);\n this.#privateClient = client;\n\n client.on(\"connected\", () => {\n if (this.#privateClient !== client) return;\n for (const h of this.#connectionHandlers) h.onConnected?.();\n });\n client.on(\"disconnected\", () => {\n if (this.#privateClient !== client) return;\n for (const h of this.#connectionHandlers) h.onDisconnected?.();\n });\n\n client.connect();\n return client;\n }\n\n #ensurePublicClient(Centrifuge: CentrifugeCtor): Centrifuge {\n return this.#publicClient ?? this.#createPublicClient(Centrifuge);\n }\n\n #ensurePrivateClient(Centrifuge: CentrifugeCtor): Centrifuge {\n if (!this.#hasAuth()) {\n throw new AuthenticationError(\n \"Cannot create authenticated realtime client without authentication\",\n );\n }\n return this.#privateClient ?? this.#createPrivateClient(Centrifuge);\n }\n\n #assertChannelAuth(channel: string): void {\n if (this.#channelKind(channel) === \"private\" && !this.#hasAuth()) {\n throw new AuthenticationError(\n `Cannot subscribe to private channel \"${channel}\" without authentication`,\n );\n }\n }\n\n #ensureClientForChannel(Centrifuge: CentrifugeCtor, channel: string): Centrifuge {\n this.#assertChannelAuth(channel);\n if (this.#channelKind(channel) === \"private\") {\n return this.#ensurePrivateClient(Centrifuge);\n }\n\n return this.#ensurePublicClient(Centrifuge);\n }\n\n #attachSubscription(shared: SharedSubscription): void {\n if (shared.sub) return;\n\n // Auth failures must surface synchronously to subscribe() callers, as\n // they did when centrifuge was imported statically.\n this.#assertChannelAuth(shared.channel);\n\n const attachmentEpoch = shared.attachmentEpoch + 1;\n shared.attachmentEpoch = attachmentEpoch;\n\n // Once the transport module is loaded, attachment stays fully\n // synchronous — only the very first attach pays the dynamic import.\n if (loadedCentrifugeCtor) {\n this.#attachSubscriptionNow(loadedCentrifugeCtor, shared, attachmentEpoch);\n return;\n }\n void this.#attachSubscriptionWhenLoaded(shared, attachmentEpoch);\n }\n\n async #attachSubscriptionWhenLoaded(\n shared: SharedSubscription,\n attachmentEpoch: number,\n ): Promise<void> {\n let Centrifuge: CentrifugeCtor;\n try {\n Centrifuge = await loadCentrifuge();\n } catch (error) {\n if (shared.attachmentEpoch !== attachmentEpoch) return;\n if (this.#sharedSubs.get(shared.channel) === shared) {\n this.#sharedSubs.delete(shared.channel);\n }\n this.#emitSubscriptionError(shared, \"transport_load\", error);\n return;\n }\n\n // The subscription may have been torn down or re-attached while the\n // transport module was loading.\n if (shared.attachmentEpoch !== attachmentEpoch || shared.sub) return;\n if (this.#sharedSubs.get(shared.channel) !== shared) return;\n\n try {\n this.#attachSubscriptionNow(Centrifuge, shared, attachmentEpoch);\n } catch (error) {\n this.#sharedSubs.delete(shared.channel);\n this.#emitSubscriptionError(shared, \"auth\", error);\n }\n }\n\n #attachSubscriptionNow(\n Centrifuge: CentrifugeCtor,\n shared: SharedSubscription,\n attachmentEpoch: number,\n ): void {\n const client = this.#ensureClientForChannel(Centrifuge, shared.channel);\n\n const sub = client.newSubscription(\n shared.channel,\n this.#subscriptionOpts(shared, attachmentEpoch),\n );\n shared.sub = sub;\n shared.client = client;\n\n sub.on(\"publication\", (ctx: PublicationContext) => {\n if (shared.sub !== sub || shared.attachmentEpoch !== attachmentEpoch) return;\n for (const handler of shared.publicationHandlers) handler(ctx.data);\n });\n sub.on(\"subscribed\", () => {\n if (shared.sub !== sub || shared.attachmentEpoch !== attachmentEpoch) return;\n const subscriptionEpoch = ++shared.subscriptionEpoch;\n for (const handler of shared.subscribedHandlers) handler(subscriptionEpoch);\n });\n sub.on(\"unsubscribed\", () => {\n if (shared.sub !== sub || shared.attachmentEpoch !== attachmentEpoch) return;\n for (const handler of shared.unsubscribedHandlers) handler();\n });\n sub.on(\"error\", (ctx: SubscriptionErrorContext) => {\n if (shared.sub !== sub || shared.attachmentEpoch !== attachmentEpoch) return;\n const sdkCtx = fromCentrifugeSubscriptionError(ctx);\n for (const handler of shared.errorHandlers) handler(sdkCtx);\n });\n\n sub.subscribe();\n }\n\n #getOrCreateSubscription(channel: string): SharedSubscription {\n const existing = this.#sharedSubs.get(channel);\n if (existing) return existing;\n\n const pending = this.#pendingTeardowns.get(channel);\n if (pending) {\n this.#pendingTeardowns.delete(channel);\n this.#sharedSubs.set(channel, pending);\n if (!pending.sub) {\n this.#attachSubscription(pending);\n } else if (pending.sub.state !== \"subscribed\") {\n pending.sub.subscribe();\n }\n return pending;\n }\n\n const shared: SharedSubscription = {\n channel,\n sub: null,\n client: null,\n attachmentEpoch: 0,\n consumers: 0,\n subscriptionEpoch: 0,\n publicationHandlers: new Set(),\n subscribedHandlers: new Set(),\n unsubscribedHandlers: new Set(),\n errorHandlers: new Set(),\n };\n\n this.#sharedSubs.set(channel, shared);\n try {\n this.#attachSubscription(shared);\n } catch (error) {\n this.#sharedSubs.delete(channel);\n throw error;\n }\n return shared;\n }\n\n #callErrorHandler(handler: ErrorHandler | undefined, ctx: SdkSubscriptionErrorContext): void {\n if (!handler) return;\n\n try {\n handler(ctx);\n } catch {\n // Keep error reporting isolated from other subscription consumers.\n }\n }\n\n #callConsumerHandler(\n channel: string,\n type: string,\n handler: () => void,\n onError?: ErrorHandler,\n ): void {\n try {\n handler();\n } catch (error) {\n this.#callErrorHandler(\n onError,\n createSdkSubscriptionErrorContext(channel, type, error),\n );\n }\n }\n\n #teardownSubscription(shared: SharedSubscription): void {\n const sub = shared.sub;\n const client = shared.client;\n const kind = this.#channelKind(shared.channel);\n shared.sub = null;\n shared.client = null;\n shared.attachmentEpoch++;\n if (!sub) return;\n\n try {\n if (sub.state !== \"unsubscribed\") {\n sub.unsubscribe();\n }\n } catch {\n // noop\n }\n try {\n client?.removeSubscription?.(sub);\n } catch {\n // noop\n }\n this.#disconnectClientIfIdle(kind);\n }\n\n #hasChannelsForKind(kind: RealtimeClientKind): boolean {\n for (const shared of this.#sharedSubs.values()) {\n if (this.#channelKind(shared.channel) === kind) return true;\n }\n for (const shared of this.#pendingTeardowns.values()) {\n if (this.#channelKind(shared.channel) === kind) return true;\n }\n return false;\n }\n\n #disconnectClientIfIdle(kind: RealtimeClientKind): void {\n if (this.#hasChannelsForKind(kind)) return;\n\n const client = kind === \"private\" ? this.#privateClient : this.#publicClient;\n if (kind === \"private\") {\n this.#privateClient = null;\n } else {\n this.#publicClient = null;\n }\n\n try {\n client?.disconnect();\n } catch {\n // noop\n }\n }\n\n #disconnect(): void {\n const publicClient = this.#publicClient;\n const privateClient = this.#privateClient;\n\n this.#pendingTeardowns.clear();\n this.#sharedSubs.clear();\n this.#publicClient = null;\n this.#privateClient = null;\n\n try {\n publicClient?.disconnect();\n } catch {\n // noop\n }\n try {\n privateClient?.disconnect();\n } catch {\n // noop\n }\n this.#connectionHandlers.clear();\n }\n\n #disconnectPrivate(): void {\n const privateClient = this.#privateClient;\n this.#privateClient = null;\n\n for (const [channel, shared] of this.#sharedSubs) {\n if (this.#channelKind(channel) !== \"private\") continue;\n this.#sharedSubs.delete(channel);\n this.#teardownSubscription(shared);\n }\n for (const [channel, shared] of this.#pendingTeardowns) {\n if (this.#channelKind(channel) !== \"private\") continue;\n this.#pendingTeardowns.delete(channel);\n this.#teardownSubscription(shared);\n }\n\n try {\n privateClient?.disconnect();\n } catch {\n // noop\n }\n }\n\n /**\n * Subscribes to a realtime channel and returns an unsubscribe function. Missing\n * authentication is reported to `onError`, or thrown synchronously when no error\n * observer is provided.\n */\n subscribe<T>(channel: string, handlers: SubscribeHandlers<T>): () => void {\n let shared: SharedSubscription;\n try {\n shared = this.#getOrCreateSubscription(channel);\n } catch (error) {\n if (!(error instanceof AuthenticationError)) throw error;\n if (!handlers.onError) throw error;\n\n const ctx = createSdkSubscriptionErrorContext(channel, \"auth\", error);\n queueMicrotask(() => this.#callErrorHandler(handlers.onError, ctx));\n return () => {};\n }\n shared.consumers++;\n\n const publicationHandler = handlers.onPublication;\n const errorHandler = handlers.onError;\n const subscribedHandler = handlers.onSubscribed;\n const unsubscribedHandler = handlers.onUnsubscribed;\n\n const onError: ErrorHandler | undefined = errorHandler\n ? (ctx) => this.#callErrorHandler(errorHandler, ctx)\n : undefined;\n const onPub: PublicationHandler = (data) => {\n this.#callConsumerHandler(\n channel,\n \"publication_handler\",\n () => publicationHandler(data as T),\n onError,\n );\n };\n let lastSubscribedEpoch = -1;\n const onSubscribed = subscribedHandler\n ? (subscriptionEpoch: number) => {\n if (subscriptionEpoch <= lastSubscribedEpoch) return;\n lastSubscribedEpoch = subscriptionEpoch;\n this.#callConsumerHandler(\n channel,\n \"subscribed_handler\",\n subscribedHandler,\n onError,\n );\n }\n : undefined;\n const onUnsubscribed = unsubscribedHandler\n ? () =>\n this.#callConsumerHandler(\n channel,\n \"unsubscribed_handler\",\n unsubscribedHandler,\n onError,\n )\n : undefined;\n\n shared.publicationHandlers.add(onPub);\n\n if (onSubscribed) shared.subscribedHandlers.add(onSubscribed);\n if (onUnsubscribed) shared.unsubscribedHandlers.add(onUnsubscribed);\n if (onError) shared.errorHandlers.add(onError);\n\n let closed = false;\n if (onSubscribed && shared.sub?.state === \"subscribed\") {\n const subscriptionEpoch = shared.subscriptionEpoch;\n queueMicrotask(() => {\n if (closed || shared.sub?.state !== \"subscribed\") return;\n onSubscribed(subscriptionEpoch);\n });\n }\n\n return () => {\n if (closed) return;\n closed = true;\n\n shared.publicationHandlers.delete(onPub);\n if (onSubscribed) shared.subscribedHandlers.delete(onSubscribed);\n if (onUnsubscribed) shared.unsubscribedHandlers.delete(onUnsubscribed);\n if (onError) shared.errorHandlers.delete(onError);\n\n shared.consumers--;\n if (shared.consumers <= 0) {\n const currentShared = this.#sharedSubs.get(channel);\n if (currentShared !== shared) return;\n\n this.#sharedSubs.delete(channel);\n this.#pendingTeardowns.set(channel, shared);\n\n queueMicrotask(() => {\n if (this.#pendingTeardowns.get(channel) !== shared) return;\n this.#pendingTeardowns.delete(channel);\n\n this.#teardownSubscription(shared);\n\n if (this.#sharedSubs.size === 0 && this.#pendingTeardowns.size === 0) {\n this.#disconnect();\n }\n });\n }\n };\n }\n\n /**\n * Connects to a realtime channel with a custom message decoder.\n */\n connectChannel<T extends DescMessage>(params: ConnectChannelParams<T>): () => void {\n return this.subscribe<Uint8Array>(params.channel, {\n onPublication: (data) => {\n let decoded: MessageShape<T>;\n try {\n decoded = fromBinary(params.schema, data);\n } catch (error) {\n this.#callErrorHandler(\n params.onError,\n createSdkSubscriptionErrorContext(params.channel, \"decode\", error),\n );\n return;\n }\n params.onPublication(decoded);\n },\n onSubscribed: params.onConnected,\n onUnsubscribed: params.onDisconnected,\n onError: params.onError,\n });\n }\n\n /**\n * Connects to a realtime channel that emits protobuf messages.\n */\n connectProtoChannel<T extends DescMessage>(params: ConnectChannelParams<T>): () => void {\n return this.subscribe<Uint8Array | MessageShape<T>>(params.channel, {\n onPublication: (data) => {\n let msg: MessageShape<T> | null;\n try {\n msg = decodeProtoFrame(params.schema, data);\n } catch (error) {\n this.#callErrorHandler(\n params.onError,\n createSdkSubscriptionErrorContext(params.channel, \"decode\", error),\n );\n return;\n }\n if (!msg) {\n this.#callErrorHandler(\n params.onError,\n createSdkSubscriptionErrorContext(\n params.channel,\n \"decode\",\n \"Unable to decode protobuf frame\",\n ),\n );\n return;\n }\n params.onPublication(msg);\n },\n onSubscribed: params.onConnected,\n onUnsubscribed: params.onDisconnected,\n onError: params.onError,\n });\n }\n\n /**\n * Disconnects the realtime client from all active channels.\n */\n disconnect(): void {\n this.#disconnect();\n }\n\n /**\n * Disconnects authenticated realtime state without touching public channels.\n */\n disconnectPrivate(): void {\n this.#disconnectPrivate();\n }\n\n get isConnected(): boolean {\n return this.#publicClient !== null || this.#privateClient !== null;\n }\n\n get activeChannels(): number {\n return this.#sharedSubs.size;\n }\n\n get totalConsumers(): number {\n let total = 0;\n for (const shared of this.#sharedSubs.values()) {\n total += shared.consumers;\n }\n return total;\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,68 @@
1
+ import { createReadyGate } from "../shared/decimal-surface.js";
2
+ import { publicationHandlerErrorContext } from "../shared/subscription-errors.js";
3
+ //#region src/realtime/ready-gated-subscription.ts
4
+ /**
5
+ * Connects a transition feed whose publications require asynchronous catalog
6
+ * readiness. If readiness fails or its bounded pre-ready queue overflows, the
7
+ * consumer is unsubscribed after receiving one error and one close callback;
8
+ * continuing after a dropped transition would expose incomplete state.
9
+ */
10
+ function connectReadyGatedProtoChannel(realtime, params) {
11
+ let unsubscribeRealtime;
12
+ let unsubscribeAfterConnect = false;
13
+ let state = "active";
14
+ let terminalCloseNotified = false;
15
+ function reportPublicationError(error) {
16
+ try {
17
+ params.onError?.(publicationHandlerErrorContext(params.channel, error));
18
+ } catch {}
19
+ }
20
+ function notifyTerminalClose() {
21
+ if (terminalCloseNotified) return;
22
+ terminalCloseNotified = true;
23
+ try {
24
+ params.onDisconnected?.();
25
+ } catch {}
26
+ }
27
+ function terminate(error) {
28
+ if (state !== "active") return;
29
+ state = "terminal";
30
+ reportPublicationError(error);
31
+ if (unsubscribeRealtime) unsubscribeRealtime();
32
+ else unsubscribeAfterConnect = true;
33
+ notifyTerminalClose();
34
+ }
35
+ const gate = createReadyGate(params.ready, {
36
+ onDeliveryError: reportPublicationError,
37
+ onTerminalError: terminate
38
+ });
39
+ try {
40
+ unsubscribeRealtime = realtime.connectProtoChannel({
41
+ channel: params.channel,
42
+ schema: params.schema,
43
+ onPublication: (data) => gate.run(() => params.onPublication(data)),
44
+ onConnected: () => {
45
+ if (state === "active") params.onConnected?.();
46
+ },
47
+ onDisconnected: () => {
48
+ if (state === "terminal") notifyTerminalClose();
49
+ else if (state === "active") params.onDisconnected?.();
50
+ },
51
+ onError: params.onError
52
+ });
53
+ } catch (error) {
54
+ gate.close();
55
+ throw error;
56
+ }
57
+ if (unsubscribeAfterConnect) unsubscribeRealtime();
58
+ return () => {
59
+ if (state !== "active") return;
60
+ state = "disposed";
61
+ gate.close();
62
+ unsubscribeRealtime?.();
63
+ };
64
+ }
65
+ //#endregion
66
+ export { connectReadyGatedProtoChannel };
67
+
68
+ //# sourceMappingURL=ready-gated-subscription.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ready-gated-subscription.js","names":[],"sources":["../../src/realtime/ready-gated-subscription.ts"],"sourcesContent":["import type { DescMessage } from \"@bufbuild/protobuf\";\nimport { createReadyGate } from \"../shared/decimal-surface.js\";\nimport { publicationHandlerErrorContext } from \"../shared/subscription-errors.js\";\nimport type { ConnectChannelParams, PolyesterRealtime } from \"./types.js\";\n\ntype ReadyGatedProtoChannelParams<TSchema extends DescMessage> = ConnectChannelParams<TSchema> & {\n ready: () => Promise<void>;\n};\n\n/**\n * Connects a transition feed whose publications require asynchronous catalog\n * readiness. If readiness fails or its bounded pre-ready queue overflows, the\n * consumer is unsubscribed after receiving one error and one close callback;\n * continuing after a dropped transition would expose incomplete state.\n */\nexport function connectReadyGatedProtoChannel<TSchema extends DescMessage>(\n realtime: PolyesterRealtime,\n params: ReadyGatedProtoChannelParams<TSchema>,\n): () => void {\n let unsubscribeRealtime: (() => void) | undefined;\n let unsubscribeAfterConnect = false;\n let state: \"active\" | \"terminal\" | \"disposed\" = \"active\";\n let terminalCloseNotified = false;\n\n function reportPublicationError(error: unknown): void {\n try {\n params.onError?.(publicationHandlerErrorContext(params.channel, error));\n } catch {\n // Keep consumer error-handler failures isolated from subscription teardown.\n }\n }\n\n function notifyTerminalClose(): void {\n if (terminalCloseNotified) return;\n terminalCloseNotified = true;\n try {\n params.onDisconnected?.();\n } catch {\n // Keep consumer close-handler failures isolated from subscription teardown.\n }\n }\n\n function terminate(error: unknown): void {\n if (state !== \"active\") return;\n state = \"terminal\";\n reportPublicationError(error);\n\n if (unsubscribeRealtime) unsubscribeRealtime();\n else unsubscribeAfterConnect = true;\n notifyTerminalClose();\n }\n\n const gate = createReadyGate(params.ready, {\n onDeliveryError: reportPublicationError,\n onTerminalError: terminate,\n });\n\n try {\n unsubscribeRealtime = realtime.connectProtoChannel({\n channel: params.channel,\n schema: params.schema,\n onPublication: (data) => gate.run(() => params.onPublication(data)),\n onConnected: () => {\n if (state === \"active\") params.onConnected?.();\n },\n onDisconnected: () => {\n if (state === \"terminal\") notifyTerminalClose();\n else if (state === \"active\") params.onDisconnected?.();\n },\n onError: params.onError,\n });\n } catch (error) {\n gate.close();\n throw error;\n }\n\n if (unsubscribeAfterConnect) unsubscribeRealtime();\n\n return () => {\n if (state !== \"active\") return;\n state = \"disposed\";\n gate.close();\n unsubscribeRealtime?.();\n };\n}\n"],"mappings":";;;;;;;;;AAeA,SAAgB,8BACZ,UACA,QACU;CACV,IAAI;CACJ,IAAI,0BAA0B;CAC9B,IAAI,QAA4C;CAChD,IAAI,wBAAwB;CAE5B,SAAS,uBAAuB,OAAsB;EAClD,IAAI;GACA,OAAO,UAAU,+BAA+B,OAAO,SAAS,KAAK,CAAC;EAC1E,QAAQ,CAER;CACJ;CAEA,SAAS,sBAA4B;EACjC,IAAI,uBAAuB;EAC3B,wBAAwB;EACxB,IAAI;GACA,OAAO,iBAAiB;EAC5B,QAAQ,CAER;CACJ;CAEA,SAAS,UAAU,OAAsB;EACrC,IAAI,UAAU,UAAU;EACxB,QAAQ;EACR,uBAAuB,KAAK;EAE5B,IAAI,qBAAqB,oBAAoB;OACxC,0BAA0B;EAC/B,oBAAoB;CACxB;CAEA,MAAM,OAAO,gBAAgB,OAAO,OAAO;EACvC,iBAAiB;EACjB,iBAAiB;CACrB,CAAC;CAED,IAAI;EACA,sBAAsB,SAAS,oBAAoB;GAC/C,SAAS,OAAO;GAChB,QAAQ,OAAO;GACf,gBAAgB,SAAS,KAAK,UAAU,OAAO,cAAc,IAAI,CAAC;GAClE,mBAAmB;IACf,IAAI,UAAU,UAAU,OAAO,cAAc;GACjD;GACA,sBAAsB;IAClB,IAAI,UAAU,YAAY,oBAAoB;SACzC,IAAI,UAAU,UAAU,OAAO,iBAAiB;GACzD;GACA,SAAS,OAAO;EACpB,CAAC;CACL,SAAS,OAAO;EACZ,KAAK,MAAM;EACX,MAAM;CACV;CAEA,IAAI,yBAAyB,oBAAoB;CAEjD,aAAa;EACT,IAAI,UAAU,UAAU;EACxB,QAAQ;EACR,KAAK,MAAM;EACX,sBAAsB;CAC1B;AACJ"}
@@ -32,7 +32,7 @@ function snapshotThenStream(params) {
32
32
  function applyBufferedAsLive() {
33
33
  const buffered = takePendingPublications();
34
34
  if (buffered.length === 0) return;
35
- params.applyLivePublications(buffered);
35
+ applyLivePublications(buffered);
36
36
  }
37
37
  function bufferPublications(publications) {
38
38
  if (params.bufferPublicationKey) {
@@ -57,6 +57,20 @@ function snapshotThenStream(params) {
57
57
  function reportBufferedPublicationError(error) {
58
58
  params.onError?.(createSdkSubscriptionErrorContext(params.channel, "publication_handler", error));
59
59
  }
60
+ function applyLivePublications(publications) {
61
+ try {
62
+ params.applyLivePublications(publications);
63
+ } catch (error) {
64
+ reportBufferedPublicationError(error);
65
+ }
66
+ }
67
+ function applySnapshot(snapshot, bufferedPublications) {
68
+ try {
69
+ params.applySnapshot(snapshot, bufferedPublications);
70
+ } catch (error) {
71
+ reportBufferedPublicationError(error);
72
+ }
73
+ }
60
74
  function scheduleSnapshotRetry() {
61
75
  const retry = params.snapshotRetry;
62
76
  if (!retry || retryAttempts >= retry.maxAttempts || disposed) return;
@@ -75,21 +89,16 @@ function snapshotThenStream(params) {
75
89
  ready = false;
76
90
  Promise.resolve().then(() => params.fetchSnapshot()).then((snapshot) => {
77
91
  if (disposed || generation !== refreshGeneration) return;
78
- const buffered = takePendingPublications();
79
- params.applySnapshot(snapshot, buffered);
92
+ applySnapshot(snapshot, takePendingPublications());
80
93
  if (disposed || generation !== refreshGeneration) return;
81
94
  ready = true;
82
95
  retryAttempts = 0;
83
96
  }).catch((error) => {
84
97
  if (disposed || generation !== refreshGeneration) return;
85
98
  reportSnapshotError(error);
86
- if (params.snapshotFailureMode === "live") {
99
+ if ((typeof params.snapshotFailureMode === "function" ? params.snapshotFailureMode(error) : params.snapshotFailureMode) === "live") {
87
100
  ready = true;
88
- try {
89
- applyBufferedAsLive();
90
- } catch (publicationError) {
91
- reportBufferedPublicationError(publicationError);
92
- }
101
+ applyBufferedAsLive();
93
102
  }
94
103
  scheduleSnapshotRetry();
95
104
  });
@@ -104,7 +113,7 @@ function snapshotThenStream(params) {
104
113
  bufferPublications(publications);
105
114
  return;
106
115
  }
107
- params.applyLivePublications(publications);
116
+ applyLivePublications(publications);
108
117
  },
109
118
  onConnected: () => {
110
119
  if (disposed) return;
@@ -1 +1 @@
1
- {"version":3,"file":"snapshot-then-stream.js","names":[],"sources":["../../src/realtime/snapshot-then-stream.ts"],"sourcesContent":["import type { DescMessage, MessageShape } from \"@bufbuild/protobuf\";\nimport {\n createSdkSubscriptionErrorContext,\n type SdkSubscriptionErrorContext,\n} from \"../shared/subscription-errors.js\";\nimport type { PolyesterRealtime } from \"./types.js\";\nimport { formatConnectError } from \"../utils/errors.js\";\nimport { isDev } from \"../utils/is-dev.js\";\n\nexport interface SnapshotThenStreamSubscription {\n unsubscribe: () => void;\n refreshSnapshot: () => void;\n isReady: () => boolean;\n isDisposed: () => boolean;\n}\n\nexport interface SnapshotThenStreamInput<TSchema extends DescMessage, TSnapshot, TPublication> {\n realtime: PolyesterRealtime;\n channel: string;\n schema: TSchema;\n maxBufferedPublications?: number;\n bufferPublicationKey?: (publication: TPublication) => PropertyKey;\n snapshotRetry?: {\n maxAttempts: number;\n delayMs: number;\n };\n snapshotFailureMode?: \"wait\" | \"live\";\n snapshotErrorLog?: string;\n fetchSnapshot: () => Promise<TSnapshot>;\n readPublication: (message: MessageShape<TSchema>) => readonly TPublication[];\n applySnapshot: (snapshot: TSnapshot, bufferedPublications: readonly TPublication[]) => void;\n applyLivePublications: (publications: readonly TPublication[]) => void;\n onOpen?: () => void;\n onClose?: () => void;\n onError?: (ctx: SdkSubscriptionErrorContext) => void;\n}\n\n/**\n * Snapshot-then-stream for feed state. By default, the buffer caps at\n * `maxBufferedPublications` and drops the OLDEST publications, which is correct for\n * ephemeral feeds where a later publication supersedes earlier ones. Keyed state can\n * provide `bufferPublicationKey` to retain the latest value for every key without FIFO\n * loss. Entity transitions (orders, triggers, balances, trades) must NOT use this —\n * their ordering contract lives in the app-side `EntityStreamStore`, which buffers\n * unbounded and replays because dropping an event loses a row transition.\n */\nexport function snapshotThenStream<TSchema extends DescMessage, TSnapshot, TPublication>(\n params: SnapshotThenStreamInput<TSchema, TSnapshot, TPublication>,\n): SnapshotThenStreamSubscription {\n const maxBufferedPublications = params.maxBufferedPublications ?? 200;\n let droppedPublications = 0;\n\n let ready = false;\n let disposed = false;\n let refreshGeneration = 0;\n let pendingPublications: TPublication[] = [];\n let retryAttempts = 0;\n let retryTimer: ReturnType<typeof setTimeout> | null = null;\n\n function reportSnapshotError(error: unknown): void {\n if (isDev() && params.snapshotErrorLog) {\n console.error(params.snapshotErrorLog, error);\n }\n params.onError?.(\n createSdkSubscriptionErrorContext(\n params.channel,\n \"snapshot\",\n formatConnectError(error, \"snapshot failed\"),\n ),\n );\n }\n\n function takePendingPublications(): TPublication[] {\n const buffered = pendingPublications;\n pendingPublications = [];\n return buffered;\n }\n\n function applyBufferedAsLive(): void {\n const buffered = takePendingPublications();\n if (buffered.length === 0) return;\n params.applyLivePublications(buffered);\n }\n\n function bufferPublications(publications: readonly TPublication[]): void {\n if (params.bufferPublicationKey) {\n const byKey = new Map<PropertyKey, TPublication>();\n for (const publication of pendingPublications) {\n byKey.set(params.bufferPublicationKey(publication), publication);\n }\n for (const publication of publications) {\n byKey.set(params.bufferPublicationKey(publication), publication);\n }\n pendingPublications = Array.from(byKey.values());\n return;\n }\n\n const next = pendingPublications.concat(publications);\n if (next.length > maxBufferedPublications) {\n droppedPublications += next.length - maxBufferedPublications;\n if (isDev()) {\n console.warn(\n `[snapshot-then-stream] ${params.channel}: buffer cap dropped ${droppedPublications} publication(s) while snapshot in flight`,\n );\n }\n }\n pendingPublications = next.slice(-maxBufferedPublications);\n }\n\n function clearRetryTimer(): void {\n if (retryTimer === null) return;\n clearTimeout(retryTimer);\n retryTimer = null;\n }\n\n function reportBufferedPublicationError(error: unknown): void {\n params.onError?.(\n createSdkSubscriptionErrorContext(params.channel, \"publication_handler\", error),\n );\n }\n\n function scheduleSnapshotRetry(): void {\n const retry = params.snapshotRetry;\n if (!retry || retryAttempts >= retry.maxAttempts || disposed) return;\n\n const delayMs = retry.delayMs * 2 ** retryAttempts;\n retryAttempts++;\n retryTimer = setTimeout(() => {\n retryTimer = null;\n refreshSnapshot(false);\n }, delayMs);\n }\n\n function refreshSnapshot(resetRetries = true): void {\n if (disposed) return;\n clearRetryTimer();\n if (resetRetries) retryAttempts = 0;\n\n const generation = ++refreshGeneration;\n ready = false;\n\n Promise.resolve()\n .then(() => params.fetchSnapshot())\n .then((snapshot) => {\n if (disposed || generation !== refreshGeneration) return;\n\n const buffered = takePendingPublications();\n params.applySnapshot(snapshot, buffered);\n\n if (disposed || generation !== refreshGeneration) return;\n ready = true;\n retryAttempts = 0;\n })\n .catch((error: unknown) => {\n if (disposed || generation !== refreshGeneration) return;\n reportSnapshotError(error);\n if (params.snapshotFailureMode === \"live\") {\n ready = true;\n try {\n applyBufferedAsLive();\n } catch (publicationError) {\n reportBufferedPublicationError(publicationError);\n }\n }\n scheduleSnapshotRetry();\n });\n }\n\n const unsubscribeRealtime = params.realtime.connectProtoChannel({\n channel: params.channel,\n schema: params.schema,\n onPublication: (message) => {\n const publications = params.readPublication(message);\n if (publications.length === 0) return;\n\n if (!ready) {\n bufferPublications(publications);\n return;\n }\n\n params.applyLivePublications(publications);\n },\n onConnected: () => {\n if (disposed) return;\n pendingPublications = [];\n refreshSnapshot();\n params.onOpen?.();\n },\n onDisconnected: () => {\n if (disposed) return;\n clearRetryTimer();\n refreshGeneration++;\n ready = false;\n pendingPublications = [];\n params.onClose?.();\n },\n onError: params.onError,\n });\n\n function unsubscribe(): void {\n disposed = true;\n clearRetryTimer();\n refreshGeneration++;\n pendingPublications = [];\n try {\n unsubscribeRealtime();\n } catch {\n // noop\n }\n }\n\n return {\n unsubscribe,\n refreshSnapshot,\n isReady: () => ready,\n isDisposed: () => disposed,\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"snapshot-then-stream.js","names":[],"sources":["../../src/realtime/snapshot-then-stream.ts"],"sourcesContent":["import type { DescMessage, MessageShape } from \"@bufbuild/protobuf\";\nimport {\n createSdkSubscriptionErrorContext,\n type SdkSubscriptionErrorContext,\n} from \"../shared/subscription-errors.js\";\nimport type { PolyesterRealtime } from \"./types.js\";\nimport { formatConnectError } from \"../utils/errors.js\";\nimport { isDev } from \"../utils/is-dev.js\";\n\nexport interface SnapshotThenStreamSubscription {\n unsubscribe: () => void;\n refreshSnapshot: () => void;\n isReady: () => boolean;\n isDisposed: () => boolean;\n}\n\nexport interface SnapshotThenStreamInput<TSchema extends DescMessage, TSnapshot, TPublication> {\n realtime: PolyesterRealtime;\n channel: string;\n schema: TSchema;\n maxBufferedPublications?: number;\n bufferPublicationKey?: (publication: TPublication) => PropertyKey;\n snapshotRetry?: {\n maxAttempts: number;\n delayMs: number;\n };\n snapshotFailureMode?: \"wait\" | \"live\" | ((error: unknown) => \"wait\" | \"live\");\n snapshotErrorLog?: string;\n fetchSnapshot: () => Promise<TSnapshot>;\n readPublication: (message: MessageShape<TSchema>) => readonly TPublication[];\n applySnapshot: (snapshot: TSnapshot, bufferedPublications: readonly TPublication[]) => void;\n applyLivePublications: (publications: readonly TPublication[]) => void;\n onOpen?: () => void;\n onClose?: () => void;\n onError?: (ctx: SdkSubscriptionErrorContext) => void;\n}\n\n/**\n * Snapshot-then-stream for feed state. By default, the buffer caps at\n * `maxBufferedPublications` and drops the OLDEST publications, which is correct for\n * ephemeral feeds where a later publication supersedes earlier ones. Keyed state can\n * provide `bufferPublicationKey` to retain the latest value for every key without FIFO\n * loss. Entity transitions (orders, triggers, balances, trades) must NOT use this —\n * their ordering contract lives in the app-side `EntityStreamStore`, which buffers\n * unbounded and replays because dropping an event loses a row transition.\n */\nexport function snapshotThenStream<TSchema extends DescMessage, TSnapshot, TPublication>(\n params: SnapshotThenStreamInput<TSchema, TSnapshot, TPublication>,\n): SnapshotThenStreamSubscription {\n const maxBufferedPublications = params.maxBufferedPublications ?? 200;\n let droppedPublications = 0;\n\n let ready = false;\n let disposed = false;\n let refreshGeneration = 0;\n let pendingPublications: TPublication[] = [];\n let retryAttempts = 0;\n let retryTimer: ReturnType<typeof setTimeout> | null = null;\n\n function reportSnapshotError(error: unknown): void {\n if (isDev() && params.snapshotErrorLog) {\n console.error(params.snapshotErrorLog, error);\n }\n params.onError?.(\n createSdkSubscriptionErrorContext(\n params.channel,\n \"snapshot\",\n formatConnectError(error, \"snapshot failed\"),\n ),\n );\n }\n\n function takePendingPublications(): TPublication[] {\n const buffered = pendingPublications;\n pendingPublications = [];\n return buffered;\n }\n\n function applyBufferedAsLive(): void {\n const buffered = takePendingPublications();\n if (buffered.length === 0) return;\n applyLivePublications(buffered);\n }\n\n function bufferPublications(publications: readonly TPublication[]): void {\n if (params.bufferPublicationKey) {\n const byKey = new Map<PropertyKey, TPublication>();\n for (const publication of pendingPublications) {\n byKey.set(params.bufferPublicationKey(publication), publication);\n }\n for (const publication of publications) {\n byKey.set(params.bufferPublicationKey(publication), publication);\n }\n pendingPublications = Array.from(byKey.values());\n return;\n }\n\n const next = pendingPublications.concat(publications);\n if (next.length > maxBufferedPublications) {\n droppedPublications += next.length - maxBufferedPublications;\n if (isDev()) {\n console.warn(\n `[snapshot-then-stream] ${params.channel}: buffer cap dropped ${droppedPublications} publication(s) while snapshot in flight`,\n );\n }\n }\n pendingPublications = next.slice(-maxBufferedPublications);\n }\n\n function clearRetryTimer(): void {\n if (retryTimer === null) return;\n clearTimeout(retryTimer);\n retryTimer = null;\n }\n\n function reportBufferedPublicationError(error: unknown): void {\n params.onError?.(\n createSdkSubscriptionErrorContext(params.channel, \"publication_handler\", error),\n );\n }\n\n function applyLivePublications(publications: readonly TPublication[]): void {\n try {\n params.applyLivePublications(publications);\n } catch (error) {\n reportBufferedPublicationError(error);\n }\n }\n\n function applySnapshot(\n snapshot: TSnapshot,\n bufferedPublications: readonly TPublication[],\n ): void {\n try {\n params.applySnapshot(snapshot, bufferedPublications);\n } catch (error) {\n reportBufferedPublicationError(error);\n }\n }\n\n function scheduleSnapshotRetry(): void {\n const retry = params.snapshotRetry;\n if (!retry || retryAttempts >= retry.maxAttempts || disposed) return;\n\n const delayMs = retry.delayMs * 2 ** retryAttempts;\n retryAttempts++;\n retryTimer = setTimeout(() => {\n retryTimer = null;\n refreshSnapshot(false);\n }, delayMs);\n }\n\n function refreshSnapshot(resetRetries = true): void {\n if (disposed) return;\n clearRetryTimer();\n if (resetRetries) retryAttempts = 0;\n\n const generation = ++refreshGeneration;\n ready = false;\n\n Promise.resolve()\n .then(() => params.fetchSnapshot())\n .then((snapshot) => {\n if (disposed || generation !== refreshGeneration) return;\n\n const buffered = takePendingPublications();\n applySnapshot(snapshot, buffered);\n\n if (disposed || generation !== refreshGeneration) return;\n ready = true;\n retryAttempts = 0;\n })\n .catch((error: unknown) => {\n if (disposed || generation !== refreshGeneration) return;\n reportSnapshotError(error);\n const failureMode =\n typeof params.snapshotFailureMode === \"function\"\n ? params.snapshotFailureMode(error)\n : params.snapshotFailureMode;\n if (failureMode === \"live\") {\n ready = true;\n applyBufferedAsLive();\n }\n scheduleSnapshotRetry();\n });\n }\n\n const unsubscribeRealtime = params.realtime.connectProtoChannel({\n channel: params.channel,\n schema: params.schema,\n onPublication: (message) => {\n const publications = params.readPublication(message);\n if (publications.length === 0) return;\n\n if (!ready) {\n bufferPublications(publications);\n return;\n }\n\n applyLivePublications(publications);\n },\n onConnected: () => {\n if (disposed) return;\n pendingPublications = [];\n refreshSnapshot();\n params.onOpen?.();\n },\n onDisconnected: () => {\n if (disposed) return;\n clearRetryTimer();\n refreshGeneration++;\n ready = false;\n pendingPublications = [];\n params.onClose?.();\n },\n onError: params.onError,\n });\n\n function unsubscribe(): void {\n disposed = true;\n clearRetryTimer();\n refreshGeneration++;\n pendingPublications = [];\n try {\n unsubscribeRealtime();\n } catch {\n // noop\n }\n }\n\n return {\n unsubscribe,\n refreshSnapshot,\n isReady: () => ready,\n isDisposed: () => disposed,\n };\n}\n"],"mappings":"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"}
@@ -39,7 +39,9 @@ declare class PolyesterServerClient extends PolyesterClient {
39
39
  get hasDisplaySession(): boolean;
40
40
  get session(): ServerSessionSnapshot;
41
41
  /**
42
- * Verifies the current server session and returns its auth state.
42
+ * Verifies the current server session. Returns null only when credentials
43
+ * are absent or rejected as unauthenticated. Transport, configuration, and
44
+ * other backend failures are rethrown so callers do not treat outages as logout.
43
45
  */
44
46
  verifySession(): Promise<Me | null>;
45
47
  }
@@ -1 +1 @@
1
- {"version":3,"file":"server-client.d.ts","names":[],"sources":["../src/server-client.ts"],"mappings":";;;;;;;;;;KAiCY,8BAA8B;;EAEtC,OAAO,kBAAkB;;EAEzB,UAAU;;;;;;;EAOV;;;;;cAMS,8BAA8B;;EAK3B,YAAA,QAAQ;;;;;;YAyBD,4BAA4B;MAc3C;MAIA;MAIA;MAIA;MAIA,WAAW;;;;EAOT,iBAAiB,QAAQ;;KAW9B,8BAA8B,WAAW,gBAAgB,4BACxD,KACI;;KAcE,sCACR,8BAA8B;;;;;;EAM1B,SAAS;;;;;;EAMT;;;KAII,sCACR,8BAA8B;EAC1B,SAAS;;;;;;EAMT;;;;;iBAMQ,uCACZ,QAAQ,sCACT;;;;iBA0Ba,uCACZ,QAAQ,sCACT"}
1
+ {"version":3,"file":"server-client.d.ts","names":[],"sources":["../src/server-client.ts"],"mappings":";;;;;;;;;;KAkCY,8BAA8B;;EAEtC,OAAO,kBAAkB;;EAEzB,UAAU;;;;;;;EAOV;;;;;cAMS,8BAA8B;;EAK3B,YAAA,QAAQ;;;;;;YAyBD,4BAA4B;MAc3C;MAIA;MAIA;MAIA;MAIA,WAAW;;;;;;EAST,iBAAiB,QAAQ;;KAa9B,8BAA8B,WAAW,gBAAgB,4BACxD,KACI;;KAcE,sCACR,8BAA8B;;;;;;EAM1B,SAAS;;;;;;EAMT;;;KAII,sCACR,8BAA8B;EAC1B,SAAS;;;;;;EAMT;;;;;iBAMQ,uCACZ,QAAQ,sCACT;;;;iBA0Ba,uCACZ,QAAQ,sCACT"}
@@ -1,4 +1,5 @@
1
- import { ConfigurationError } from "./shared/errors.js";
1
+ import { AuthenticationError, ConfigurationError } from "./shared/errors.js";
2
+ import { toPolyesterError } from "./shared/connect-error-mapping.js";
2
3
  import { PolyesterClient, parsePolyesterClientConfig, pickPolyesterCatalogConfig } from "./core-client.js";
3
4
  import "./services/auth/cookie-constants.js";
4
5
  import { emptyServerSessionSnapshot, parseServerSessionSnapshot } from "./services/auth/session.js";
@@ -67,14 +68,18 @@ var PolyesterServerClient = class extends PolyesterClient {
67
68
  return this.#session;
68
69
  }
69
70
  /**
70
- * Verifies the current server session and returns its auth state.
71
+ * Verifies the current server session. Returns null only when credentials
72
+ * are absent or rejected as unauthenticated. Transport, configuration, and
73
+ * other backend failures are rethrown so callers do not treat outages as logout.
71
74
  */
72
75
  async verifySession() {
73
76
  if (!this.#hasAuthProvider) return null;
74
77
  try {
75
78
  return await this.auth.me();
76
- } catch {
77
- return null;
79
+ } catch (error) {
80
+ const mappedError = toPolyesterError(error);
81
+ if (mappedError instanceof AuthenticationError) return null;
82
+ throw mappedError;
78
83
  }
79
84
  }
80
85
  };
@@ -1 +1 @@
1
- {"version":3,"file":"server-client.js","names":["#hasAuthProvider","#session","#useDisplaySessionActiveAccountAsDefault"],"sources":["../src/server-client.ts"],"sourcesContent":["import {\n parsePolyesterClientConfig,\n pickPolyesterCatalogConfig,\n PolyesterClient,\n type PolyesterClientBaseConfig,\n} from \"./core-client.js\";\nimport {\n POLYESTER_AUTH_TOKEN_COOKIE_NAME,\n POLYESTER_SESSION_COOKIE_NAME,\n} from \"./services/auth/cookie-constants.js\";\nimport type { ServerSessionSnapshot } from \"./services/auth/session.types.js\";\nimport { emptyServerSessionSnapshot, parseServerSessionSnapshot } from \"./services/auth/session.js\";\nimport type { CookieGetter } from \"./utils/cookies.js\";\nimport type { JwtAuthProvider, ApiKeyEd25519AuthProvider } from \"./shared/transports.js\";\nimport type { SubaccountResolver } from \"./services/subaccount-resolver.js\";\nimport { isJwtValid } from \"./utils/jwt.js\";\nimport type { Me } from \"./services/auth/auth.js\";\nimport type { PolyesterEnvironment } from \"./environment.js\";\nimport { ConfigurationError } from \"./shared/errors.js\";\n\nexport type { ServerSessionSnapshot };\n\n/**\n * Parses a serialized session cookie into SDK session data.\n */\nexport function parseSessionCookie(\n cookies: CookieGetter,\n environment: PolyesterEnvironment,\n): ServerSessionSnapshot {\n return parseServerSessionSnapshot(cookies, environment);\n}\n\n/** Configuration for the server Polyester client. */\nexport type PolyesterServerClientConfig = PolyesterClientBaseConfig & {\n /** Auth provider config for HTTP/Connect endpoints. */\n auth?: JwtAuthProvider | ApiKeyEd25519AuthProvider;\n /** Bearer authentication and display-only session data parsed from cookies. */\n session?: ServerSessionSnapshot;\n /**\n * Use unsigned display-session `activeAccount` as the default subaccount for\n * calls that omit `subaccountId`. This preserves account-switcher intent for\n * apps that explicitly opt in; backend authorization must still decide what\n * the authenticated user can access.\n */\n useDisplaySessionActiveAccountAsDefault?: boolean;\n};\n\n/**\n * Server-side SDK client that can parse display-session cookies and verify bearer-token sessions with the backend.\n */\nexport class PolyesterServerClient extends PolyesterClient {\n #hasAuthProvider: boolean;\n #session: ServerSessionSnapshot;\n #useDisplaySessionActiveAccountAsDefault: boolean;\n\n constructor(config: PolyesterServerClientConfig) {\n config = parsePolyesterClientConfig(config);\n const auth = config.auth ?? undefined;\n\n super({\n environment: config.environment,\n interceptors: config.interceptors,\n auth,\n wireFormat: config.wireFormat,\n realtime: config.realtime,\n ...pickPolyesterCatalogConfig(config),\n transports: config.transports,\n realtimeClient: config.realtimeClient,\n });\n this.#hasAuthProvider = !!auth;\n this.#session = config.session ?? emptyServerSessionSnapshot();\n this.#useDisplaySessionActiveAccountAsDefault =\n config.useDisplaySessionActiveAccountAsDefault ?? false;\n }\n\n /**\n * Creates a resolver for server-side subaccount defaults. Display-session\n * `activeAccount` is only used as caller intent when explicitly enabled;\n * backend auth remains authoritative.\n */\n protected override createSubaccountResolver(): SubaccountResolver {\n return {\n getDefaultSubaccountId: () => {\n if (!this.#useDisplaySessionActiveAccountAsDefault) return null;\n\n const activeAccount = this.#session.activeAccount;\n if (activeAccount && !activeAccount.isMain) return activeAccount.accountId;\n return null;\n },\n getActiveAccountId: () => this.#session.activeAccount?.accountId ?? null,\n getMainAccountId: () => this.#session.activeAccount?.mainAccountId ?? null,\n };\n }\n\n get hasAuthProvider(): boolean {\n return this.#hasAuthProvider;\n }\n\n get hasBearerToken(): boolean {\n return !!this.#session.bearerToken;\n }\n\n get hasUsableBearerToken(): boolean {\n return isJwtValid(this.#session.bearerToken);\n }\n\n get hasDisplaySession(): boolean {\n return this.#session.hasDisplaySession;\n }\n\n get session(): ServerSessionSnapshot {\n return this.#session;\n }\n\n /**\n * Verifies the current server session and returns its auth state.\n */\n async verifySession(): Promise<Me | null> {\n if (!this.#hasAuthProvider) return null;\n\n try {\n return await this.auth.me();\n } catch {\n return null;\n }\n }\n}\n\ntype ServerClientFactoryBaseConfig<TConfig> = TConfig extends PolyesterClientBaseConfig\n ? Pick<\n TConfig,\n | \"environment\"\n | \"interceptors\"\n | \"realtime\"\n | \"wireFormat\"\n | \"catalog\"\n | \"catalogSnapshot\"\n | \"catalogCell\"\n | \"transports\"\n | \"realtimeClient\"\n >\n : never;\n\n/** Parameters for creating a server client from cookies. */\nexport type CreateServerClientFromCookiesParams =\n ServerClientFactoryBaseConfig<PolyesterClientBaseConfig> & {\n /**\n * A Request, name-value record, or synchronous cookie store whose `get`\n * method returns either a string or an object with a string `value`.\n * Await asynchronous framework cookie helpers before passing their result.\n */\n cookies: CookieGetter;\n /**\n * Use unsigned display-session `activeAccount` as the default subaccount for\n * calls that omit `subaccountId`. This is caller intent from UI hydration,\n * not proof of authority; backend authorization remains authoritative.\n */\n useDisplaySessionActiveAccountAsDefault?: boolean;\n };\n\n/** Parameters for creating a server client from a request. */\nexport type CreateServerClientFromRequestParams =\n ServerClientFactoryBaseConfig<PolyesterClientBaseConfig> & {\n request: Request;\n /**\n * Use unsigned display-session `activeAccount` as the default subaccount for\n * calls that omit `subaccountId`. This is caller intent from UI hydration,\n * not proof of authority; backend authorization remains authoritative.\n */\n useDisplaySessionActiveAccountAsDefault?: boolean;\n };\n\n/**\n * Creates a server SDK client from a Request, cookie record, or synchronous cookie store.\n */\nexport function createPolyesterServerClientFromCookies(\n params: CreateServerClientFromCookiesParams,\n): PolyesterServerClient {\n const session = parseSessionCookie(params.cookies, params.environment);\n const auth = isJwtValid(session.bearerToken)\n ? ({\n kind: \"jwt\",\n getToken: () => session.bearerToken,\n } satisfies JwtAuthProvider)\n : undefined;\n\n return new PolyesterServerClient({\n environment: params.environment,\n interceptors: params.interceptors,\n session,\n wireFormat: params.wireFormat,\n realtime: params.realtime,\n ...pickPolyesterCatalogConfig(params),\n transports: params.transports,\n realtimeClient: params.realtimeClient,\n auth,\n useDisplaySessionActiveAccountAsDefault: params.useDisplaySessionActiveAccountAsDefault,\n });\n}\n\n/**\n * Creates a server SDK client from a Request object.\n */\nexport function createPolyesterServerClientFromRequest(\n params: CreateServerClientFromRequestParams,\n): PolyesterServerClient {\n if (!(params?.request instanceof Request)) {\n throw new ConfigurationError(\"request is required and must be a Request.\");\n }\n return createPolyesterServerClientFromCookies({\n cookies: params.request,\n environment: params.environment,\n interceptors: params.interceptors,\n wireFormat: params.wireFormat,\n realtime: params.realtime,\n ...pickPolyesterCatalogConfig(params),\n transports: params.transports,\n realtimeClient: params.realtimeClient,\n useDisplaySessionActiveAccountAsDefault: params.useDisplaySessionActiveAccountAsDefault,\n });\n}\n\n/**\n * Reads a bearer token from supported auth cookies.\n */\nexport function getBearerTokenFromCookies(\n cookies: CookieGetter,\n environment: PolyesterEnvironment,\n): string | null {\n return parseSessionCookie(cookies, environment).bearerToken;\n}\n\nexport { POLYESTER_AUTH_TOKEN_COOKIE_NAME, POLYESTER_SESSION_COOKIE_NAME };\n"],"mappings":";;;;;;;;;AAyBA,SAAgB,mBACZ,SACA,aACqB;CACrB,OAAO,2BAA2B,SAAS,WAAW;AAC1D;;;;AAoBA,IAAa,wBAAb,cAA2C,gBAAgB;CACvD;CACA;CACA;CAEA,YAAY,QAAqC;EAC7C,SAAS,2BAA2B,MAAM;EAC1C,MAAM,OAAO,OAAO,QAAQ,KAAA;EAE5B,MAAM;GACF,aAAa,OAAO;GACpB,cAAc,OAAO;GACrB;GACA,YAAY,OAAO;GACnB,UAAU,OAAO;GACjB,GAAG,2BAA2B,MAAM;GACpC,YAAY,OAAO;GACnB,gBAAgB,OAAO;EAC3B,CAAC;EACD,KAAKA,mBAAmB,CAAC,CAAC;EAC1B,KAAKC,WAAW,OAAO,WAAW,2BAA2B;EAC7D,KAAKC,2CACD,OAAO,2CAA2C;CAC1D;;;;;;CAOA,2BAAkE;EAC9D,OAAO;GACH,8BAA8B;IAC1B,IAAI,CAAC,KAAKA,0CAA0C,OAAO;IAE3D,MAAM,gBAAgB,KAAKD,SAAS;IACpC,IAAI,iBAAiB,CAAC,cAAc,QAAQ,OAAO,cAAc;IACjE,OAAO;GACX;GACA,0BAA0B,KAAKA,SAAS,eAAe,aAAa;GACpE,wBAAwB,KAAKA,SAAS,eAAe,iBAAiB;EAC1E;CACJ;CAEA,IAAI,kBAA2B;EAC3B,OAAO,KAAKD;CAChB;CAEA,IAAI,iBAA0B;EAC1B,OAAO,CAAC,CAAC,KAAKC,SAAS;CAC3B;CAEA,IAAI,uBAAgC;EAChC,OAAO,WAAW,KAAKA,SAAS,WAAW;CAC/C;CAEA,IAAI,oBAA6B;EAC7B,OAAO,KAAKA,SAAS;CACzB;CAEA,IAAI,UAAiC;EACjC,OAAO,KAAKA;CAChB;;;;CAKA,MAAM,gBAAoC;EACtC,IAAI,CAAC,KAAKD,kBAAkB,OAAO;EAEnC,IAAI;GACA,OAAO,MAAM,KAAK,KAAK,GAAG;EAC9B,QAAQ;GACJ,OAAO;EACX;CACJ;AACJ;;;;AAiDA,SAAgB,uCACZ,QACqB;CACrB,MAAM,UAAU,mBAAmB,OAAO,SAAS,OAAO,WAAW;CACrE,MAAM,OAAO,WAAW,QAAQ,WAAW,IACpC;EACG,MAAM;EACN,gBAAgB,QAAQ;CAC5B,IACA,KAAA;CAEN,OAAO,IAAI,sBAAsB;EAC7B,aAAa,OAAO;EACpB,cAAc,OAAO;EACrB;EACA,YAAY,OAAO;EACnB,UAAU,OAAO;EACjB,GAAG,2BAA2B,MAAM;EACpC,YAAY,OAAO;EACnB,gBAAgB,OAAO;EACvB;EACA,yCAAyC,OAAO;CACpD,CAAC;AACL;;;;AAKA,SAAgB,uCACZ,QACqB;CACrB,IAAI,EAAE,QAAQ,mBAAmB,UAC7B,MAAM,IAAI,mBAAmB,4CAA4C;CAE7E,OAAO,uCAAuC;EAC1C,SAAS,OAAO;EAChB,aAAa,OAAO;EACpB,cAAc,OAAO;EACrB,YAAY,OAAO;EACnB,UAAU,OAAO;EACjB,GAAG,2BAA2B,MAAM;EACpC,YAAY,OAAO;EACnB,gBAAgB,OAAO;EACvB,yCAAyC,OAAO;CACpD,CAAC;AACL"}
1
+ {"version":3,"file":"server-client.js","names":["#hasAuthProvider","#session","#useDisplaySessionActiveAccountAsDefault"],"sources":["../src/server-client.ts"],"sourcesContent":["import {\n parsePolyesterClientConfig,\n pickPolyesterCatalogConfig,\n PolyesterClient,\n type PolyesterClientBaseConfig,\n} from \"./core-client.js\";\nimport {\n POLYESTER_AUTH_TOKEN_COOKIE_NAME,\n POLYESTER_SESSION_COOKIE_NAME,\n} from \"./services/auth/cookie-constants.js\";\nimport type { ServerSessionSnapshot } from \"./services/auth/session.types.js\";\nimport { emptyServerSessionSnapshot, parseServerSessionSnapshot } from \"./services/auth/session.js\";\nimport type { CookieGetter } from \"./utils/cookies.js\";\nimport type { JwtAuthProvider, ApiKeyEd25519AuthProvider } from \"./shared/transports.js\";\nimport type { SubaccountResolver } from \"./services/subaccount-resolver.js\";\nimport { isJwtValid } from \"./utils/jwt.js\";\nimport type { Me } from \"./services/auth/auth.js\";\nimport type { PolyesterEnvironment } from \"./environment.js\";\nimport { AuthenticationError, ConfigurationError } from \"./shared/errors.js\";\nimport { toPolyesterError } from \"./shared/connect-error-mapping.js\";\n\nexport type { ServerSessionSnapshot };\n\n/**\n * Parses a serialized session cookie into SDK session data.\n */\nexport function parseSessionCookie(\n cookies: CookieGetter,\n environment: PolyesterEnvironment,\n): ServerSessionSnapshot {\n return parseServerSessionSnapshot(cookies, environment);\n}\n\n/** Configuration for the server Polyester client. */\nexport type PolyesterServerClientConfig = PolyesterClientBaseConfig & {\n /** Auth provider config for HTTP/Connect endpoints. */\n auth?: JwtAuthProvider | ApiKeyEd25519AuthProvider;\n /** Bearer authentication and display-only session data parsed from cookies. */\n session?: ServerSessionSnapshot;\n /**\n * Use unsigned display-session `activeAccount` as the default subaccount for\n * calls that omit `subaccountId`. This preserves account-switcher intent for\n * apps that explicitly opt in; backend authorization must still decide what\n * the authenticated user can access.\n */\n useDisplaySessionActiveAccountAsDefault?: boolean;\n};\n\n/**\n * Server-side SDK client that can parse display-session cookies and verify bearer-token sessions with the backend.\n */\nexport class PolyesterServerClient extends PolyesterClient {\n #hasAuthProvider: boolean;\n #session: ServerSessionSnapshot;\n #useDisplaySessionActiveAccountAsDefault: boolean;\n\n constructor(config: PolyesterServerClientConfig) {\n config = parsePolyesterClientConfig(config);\n const auth = config.auth ?? undefined;\n\n super({\n environment: config.environment,\n interceptors: config.interceptors,\n auth,\n wireFormat: config.wireFormat,\n realtime: config.realtime,\n ...pickPolyesterCatalogConfig(config),\n transports: config.transports,\n realtimeClient: config.realtimeClient,\n });\n this.#hasAuthProvider = !!auth;\n this.#session = config.session ?? emptyServerSessionSnapshot();\n this.#useDisplaySessionActiveAccountAsDefault =\n config.useDisplaySessionActiveAccountAsDefault ?? false;\n }\n\n /**\n * Creates a resolver for server-side subaccount defaults. Display-session\n * `activeAccount` is only used as caller intent when explicitly enabled;\n * backend auth remains authoritative.\n */\n protected override createSubaccountResolver(): SubaccountResolver {\n return {\n getDefaultSubaccountId: () => {\n if (!this.#useDisplaySessionActiveAccountAsDefault) return null;\n\n const activeAccount = this.#session.activeAccount;\n if (activeAccount && !activeAccount.isMain) return activeAccount.accountId;\n return null;\n },\n getActiveAccountId: () => this.#session.activeAccount?.accountId ?? null,\n getMainAccountId: () => this.#session.activeAccount?.mainAccountId ?? null,\n };\n }\n\n get hasAuthProvider(): boolean {\n return this.#hasAuthProvider;\n }\n\n get hasBearerToken(): boolean {\n return !!this.#session.bearerToken;\n }\n\n get hasUsableBearerToken(): boolean {\n return isJwtValid(this.#session.bearerToken);\n }\n\n get hasDisplaySession(): boolean {\n return this.#session.hasDisplaySession;\n }\n\n get session(): ServerSessionSnapshot {\n return this.#session;\n }\n\n /**\n * Verifies the current server session. Returns null only when credentials\n * are absent or rejected as unauthenticated. Transport, configuration, and\n * other backend failures are rethrown so callers do not treat outages as logout.\n */\n async verifySession(): Promise<Me | null> {\n if (!this.#hasAuthProvider) return null;\n\n try {\n return await this.auth.me();\n } catch (error) {\n const mappedError = toPolyesterError(error);\n if (mappedError instanceof AuthenticationError) return null;\n throw mappedError;\n }\n }\n}\n\ntype ServerClientFactoryBaseConfig<TConfig> = TConfig extends PolyesterClientBaseConfig\n ? Pick<\n TConfig,\n | \"environment\"\n | \"interceptors\"\n | \"realtime\"\n | \"wireFormat\"\n | \"catalog\"\n | \"catalogSnapshot\"\n | \"catalogCell\"\n | \"transports\"\n | \"realtimeClient\"\n >\n : never;\n\n/** Parameters for creating a server client from cookies. */\nexport type CreateServerClientFromCookiesParams =\n ServerClientFactoryBaseConfig<PolyesterClientBaseConfig> & {\n /**\n * A Request, name-value record, or synchronous cookie store whose `get`\n * method returns either a string or an object with a string `value`.\n * Await asynchronous framework cookie helpers before passing their result.\n */\n cookies: CookieGetter;\n /**\n * Use unsigned display-session `activeAccount` as the default subaccount for\n * calls that omit `subaccountId`. This is caller intent from UI hydration,\n * not proof of authority; backend authorization remains authoritative.\n */\n useDisplaySessionActiveAccountAsDefault?: boolean;\n };\n\n/** Parameters for creating a server client from a request. */\nexport type CreateServerClientFromRequestParams =\n ServerClientFactoryBaseConfig<PolyesterClientBaseConfig> & {\n request: Request;\n /**\n * Use unsigned display-session `activeAccount` as the default subaccount for\n * calls that omit `subaccountId`. This is caller intent from UI hydration,\n * not proof of authority; backend authorization remains authoritative.\n */\n useDisplaySessionActiveAccountAsDefault?: boolean;\n };\n\n/**\n * Creates a server SDK client from a Request, cookie record, or synchronous cookie store.\n */\nexport function createPolyesterServerClientFromCookies(\n params: CreateServerClientFromCookiesParams,\n): PolyesterServerClient {\n const session = parseSessionCookie(params.cookies, params.environment);\n const auth = isJwtValid(session.bearerToken)\n ? ({\n kind: \"jwt\",\n getToken: () => session.bearerToken,\n } satisfies JwtAuthProvider)\n : undefined;\n\n return new PolyesterServerClient({\n environment: params.environment,\n interceptors: params.interceptors,\n session,\n wireFormat: params.wireFormat,\n realtime: params.realtime,\n ...pickPolyesterCatalogConfig(params),\n transports: params.transports,\n realtimeClient: params.realtimeClient,\n auth,\n useDisplaySessionActiveAccountAsDefault: params.useDisplaySessionActiveAccountAsDefault,\n });\n}\n\n/**\n * Creates a server SDK client from a Request object.\n */\nexport function createPolyesterServerClientFromRequest(\n params: CreateServerClientFromRequestParams,\n): PolyesterServerClient {\n if (!(params?.request instanceof Request)) {\n throw new ConfigurationError(\"request is required and must be a Request.\");\n }\n return createPolyesterServerClientFromCookies({\n cookies: params.request,\n environment: params.environment,\n interceptors: params.interceptors,\n wireFormat: params.wireFormat,\n realtime: params.realtime,\n ...pickPolyesterCatalogConfig(params),\n transports: params.transports,\n realtimeClient: params.realtimeClient,\n useDisplaySessionActiveAccountAsDefault: params.useDisplaySessionActiveAccountAsDefault,\n });\n}\n\n/**\n * Reads a bearer token from supported auth cookies.\n */\nexport function getBearerTokenFromCookies(\n cookies: CookieGetter,\n environment: PolyesterEnvironment,\n): string | null {\n return parseSessionCookie(cookies, environment).bearerToken;\n}\n\nexport { POLYESTER_AUTH_TOKEN_COOKIE_NAME, POLYESTER_SESSION_COOKIE_NAME };\n"],"mappings":";;;;;;;;;;AA0BA,SAAgB,mBACZ,SACA,aACqB;CACrB,OAAO,2BAA2B,SAAS,WAAW;AAC1D;;;;AAoBA,IAAa,wBAAb,cAA2C,gBAAgB;CACvD;CACA;CACA;CAEA,YAAY,QAAqC;EAC7C,SAAS,2BAA2B,MAAM;EAC1C,MAAM,OAAO,OAAO,QAAQ,KAAA;EAE5B,MAAM;GACF,aAAa,OAAO;GACpB,cAAc,OAAO;GACrB;GACA,YAAY,OAAO;GACnB,UAAU,OAAO;GACjB,GAAG,2BAA2B,MAAM;GACpC,YAAY,OAAO;GACnB,gBAAgB,OAAO;EAC3B,CAAC;EACD,KAAKA,mBAAmB,CAAC,CAAC;EAC1B,KAAKC,WAAW,OAAO,WAAW,2BAA2B;EAC7D,KAAKC,2CACD,OAAO,2CAA2C;CAC1D;;;;;;CAOA,2BAAkE;EAC9D,OAAO;GACH,8BAA8B;IAC1B,IAAI,CAAC,KAAKA,0CAA0C,OAAO;IAE3D,MAAM,gBAAgB,KAAKD,SAAS;IACpC,IAAI,iBAAiB,CAAC,cAAc,QAAQ,OAAO,cAAc;IACjE,OAAO;GACX;GACA,0BAA0B,KAAKA,SAAS,eAAe,aAAa;GACpE,wBAAwB,KAAKA,SAAS,eAAe,iBAAiB;EAC1E;CACJ;CAEA,IAAI,kBAA2B;EAC3B,OAAO,KAAKD;CAChB;CAEA,IAAI,iBAA0B;EAC1B,OAAO,CAAC,CAAC,KAAKC,SAAS;CAC3B;CAEA,IAAI,uBAAgC;EAChC,OAAO,WAAW,KAAKA,SAAS,WAAW;CAC/C;CAEA,IAAI,oBAA6B;EAC7B,OAAO,KAAKA,SAAS;CACzB;CAEA,IAAI,UAAiC;EACjC,OAAO,KAAKA;CAChB;;;;;;CAOA,MAAM,gBAAoC;EACtC,IAAI,CAAC,KAAKD,kBAAkB,OAAO;EAEnC,IAAI;GACA,OAAO,MAAM,KAAK,KAAK,GAAG;EAC9B,SAAS,OAAO;GACZ,MAAM,cAAc,iBAAiB,KAAK;GAC1C,IAAI,uBAAuB,qBAAqB,OAAO;GACvD,MAAM;EACV;CACJ;AACJ;;;;AAiDA,SAAgB,uCACZ,QACqB;CACrB,MAAM,UAAU,mBAAmB,OAAO,SAAS,OAAO,WAAW;CACrE,MAAM,OAAO,WAAW,QAAQ,WAAW,IACpC;EACG,MAAM;EACN,gBAAgB,QAAQ;CAC5B,IACA,KAAA;CAEN,OAAO,IAAI,sBAAsB;EAC7B,aAAa,OAAO;EACpB,cAAc,OAAO;EACrB;EACA,YAAY,OAAO;EACnB,UAAU,OAAO;EACjB,GAAG,2BAA2B,MAAM;EACpC,YAAY,OAAO;EACnB,gBAAgB,OAAO;EACvB;EACA,yCAAyC,OAAO;CACpD,CAAC;AACL;;;;AAKA,SAAgB,uCACZ,QACqB;CACrB,IAAI,EAAE,QAAQ,mBAAmB,UAC7B,MAAM,IAAI,mBAAmB,4CAA4C;CAE7E,OAAO,uCAAuC;EAC1C,SAAS,OAAO;EAChB,aAAa,OAAO;EACpB,cAAc,OAAO;EACrB,YAAY,OAAO;EACnB,UAAU,OAAO;EACjB,GAAG,2BAA2B,MAAM;EACpC,YAAY,OAAO;EACnB,gBAAgB,OAAO;EACvB,yCAAyC,OAAO;CACpD,CAAC;AACL"}
@@ -1 +1 @@
1
- {"version":3,"file":"session.d.ts","names":[],"sources":["../../../src/services/auth/session.ts"],"mappings":";;;;;UAyBiB;EACb;;UAGa;EACb;EACA,cAAc;EACd,UAAU;EACV,aAAa;EACb;EACA;EACA;EACA,gBAAgB;EAChB;;UAGa;EACb,UAAU;EACV,aAAa;EACb;EACA;EACA;EACA;;iBAoEY,8BAA8B;;;;iBAgB9B,2BACZ,SAAS,cACT,aAAa,uBACd;;;;cA+BU;;EAIG,cAAE;IAA4B;;EAK1C,OAAO;EAUP,yBAAyB,cAAc;EAYvC,YAAY,QAAQ,0BAA0B,cAAc;EAsB5D,cAAc,QAAQ,qBAAqB,UAAU,uBAAuB;EA8B5E,YAAY,yBAAyB,UAAU;EAO/C,iBACI,eAAe,KAAK,qCACpB,UAAU;EAoBd"}
1
+ {"version":3,"file":"session.d.ts","names":[],"sources":["../../../src/services/auth/session.ts"],"mappings":";;;;;UAyBiB;EACb;;UAGa;EACb;EACA,cAAc;EACd,UAAU;EACV,aAAa;EACb;EACA;EACA;EACA,gBAAgB;EAChB;;UAGa;EACb,UAAU;EACV,aAAa;EACb;EACA;EACA;EACA;;iBAoEY,8BAA8B;;;;iBAgB9B,2BACZ,SAAS,cACT,aAAa,uBACd;;;;cA+BU;;EAIG,cAAE;IAA4B;;EAK1C,OAAO;EAUP,yBAAyB,cAAc;EAgBvC,YAAY,QAAQ,0BAA0B,cAAc;EAsB5D,cAAc,QAAQ,qBAAqB,UAAU,uBAAuB;EA8B5E,YAAY,yBAAyB,UAAU;EAO/C,iBACI,eAAe,KAAK,qCACpB,UAAU;EAoBd"}
@@ -117,7 +117,10 @@ var AuthSessionStore = class {
117
117
  getEnvironmentBoundToken(tokenStorage) {
118
118
  const token = tokenStorage.get();
119
119
  if (!token) return null;
120
- if (!this.get()) {
120
+ const session = this.#session.get();
121
+ if (!session) return token;
122
+ if (session.environmentFingerprint !== this.#environmentFingerprint) {
123
+ this.#session.clear();
121
124
  tokenStorage.clear();
122
125
  return null;
123
126
  }
@@ -1 +1 @@
1
- {"version":3,"file":"session.js","names":["#lastRawValue","#lastDecoded","#environmentFingerprint","#session"],"sources":["../../../src/services/auth/session.ts"],"sourcesContent":["import {\n setCookie,\n deleteCookie,\n getCookie,\n getCookieValue,\n shouldSecureCookies,\n type CookieGetter,\n} from \"../../utils/cookies.js\";\nimport {\n POLYESTER_AUTH_TOKEN_COOKIE_NAME,\n POLYESTER_SESSION_COOKIE_NAME,\n POLYESTER_LOGIN_COOKIE_MAX_AGE,\n} from \"./cookie-constants.js\";\nimport { SessionCodec } from \"./session-codec.js\";\nimport type { PolyesterEnvironment } from \"../../environment.js\";\nimport type { AuthTokenStorage, AuthTokenStorageSetOptions } from \"./token-storage.js\";\nimport type {\n ActiveAccountInfo,\n AuthLoginMethod,\n ServerSessionSnapshot,\n SessionData,\n} from \"./session.types.js\";\n\nexport type { ActiveAccountInfo, AuthLoginMethod, ServerSessionSnapshot, SessionData };\n\nexport interface SessionCookieOptions {\n maxAgeSeconds?: number | null;\n}\n\nexport interface CommitLoginSessionParams {\n accessToken: string;\n tokenOptions: AuthTokenStorageSetOptions;\n provider: SessionData[\"provider\"];\n loginMethod: AuthLoginMethod | null;\n primaryWallet: string;\n smartAccount: string;\n accountId: string;\n activeAccount?: ActiveAccountInfo;\n username?: string;\n}\n\nexport interface EnsureSessionParams {\n provider: SessionData[\"provider\"];\n loginMethod: AuthLoginMethod | null;\n primaryWallet: string;\n smartAccount: string;\n accountId: string;\n username?: string;\n}\n\nfunction resolveSessionCookieMaxAge(options?: SessionCookieOptions): number | undefined {\n if (!options) return POLYESTER_LOGIN_COOKIE_MAX_AGE;\n return options.maxAgeSeconds ?? undefined;\n}\n\n/**\n * Reads and writes the browser display-session cookie.\n */\nclass PolyesterSessionManager {\n /**\n * Replaces the current session state.\n */\n set(session: SessionData, options?: SessionCookieOptions): void {\n setCookie({\n name: POLYESTER_SESSION_COOKIE_NAME,\n value: SessionCodec.encode(session),\n options: {\n path: \"/\",\n maxAge: resolveSessionCookieMaxAge(options),\n secure: shouldSecureCookies(),\n sameSite: \"lax\",\n },\n });\n }\n\n // The session is re-read on every authenticated RPC; memoize the decode\n // (JSON.parse + schema validation) on the raw cookie value.\n #lastRawValue: string | null = null;\n #lastDecoded: SessionData | null = null;\n\n /**\n * Returns the current session state, or null when no session is active.\n */\n get(): SessionData | null {\n const value = getCookie(POLYESTER_SESSION_COOKIE_NAME);\n if (!value) {\n this.#lastRawValue = null;\n this.#lastDecoded = null;\n return null;\n }\n if (value === this.#lastRawValue) return this.#lastDecoded;\n\n const session = SessionCodec.decode(value);\n if (!session) {\n this.clear();\n this.#lastRawValue = null;\n this.#lastDecoded = null;\n return null;\n }\n\n this.#lastRawValue = value;\n this.#lastDecoded = session;\n return session;\n }\n\n /**\n * Clears the current session state.\n */\n clear(): void {\n deleteCookie(POLYESTER_SESSION_COOKIE_NAME);\n }\n}\n\nexport const polyesterSession = new PolyesterSessionManager();\n\nexport function emptyServerSessionSnapshot(): ServerSessionSnapshot {\n return {\n environmentFingerprint: null,\n hasDisplaySession: false,\n provider: null,\n loginMethod: null,\n accountAddresses: null,\n activeAccount: null,\n bearerToken: null,\n username: null,\n };\n}\n\n/**\n * Parses bearer authentication and display-only session cookies into a server snapshot.\n */\nexport function parseServerSessionSnapshot(\n cookies: CookieGetter,\n environment: PolyesterEnvironment,\n): ServerSessionSnapshot {\n const sessionValue = getCookieValue(cookies, POLYESTER_SESSION_COOKIE_NAME);\n const bearerToken = getCookieValue(cookies, POLYESTER_AUTH_TOKEN_COOKIE_NAME) ?? null;\n const session = sessionValue ? SessionCodec.decode(sessionValue) : null;\n\n if (!session) {\n return { ...emptyServerSessionSnapshot(), bearerToken };\n }\n\n if (session.environmentFingerprint !== environment.fingerprint) {\n return emptyServerSessionSnapshot();\n }\n\n return {\n environmentFingerprint: session.environmentFingerprint,\n hasDisplaySession: true,\n provider: session.provider,\n loginMethod: session.loginMethod,\n accountAddresses: {\n ownerAddress: session.primaryWallet,\n accountAddress: session.smartAccount,\n },\n activeAccount: session.activeAccount ?? null,\n bearerToken,\n username: session.username ?? null,\n };\n}\n\n/**\n * Owns environment-bound auth session transitions for browser clients.\n */\nexport class AuthSessionStore {\n #environmentFingerprint: string;\n #session: PolyesterSessionManager;\n\n constructor({ environmentFingerprint }: { environmentFingerprint: string }) {\n this.#environmentFingerprint = environmentFingerprint;\n this.#session = polyesterSession;\n }\n\n get(): SessionData | null {\n const session = this.#session.get();\n if (!session) return null;\n if (session.environmentFingerprint !== this.#environmentFingerprint) {\n this.#session.clear();\n return null;\n }\n return session;\n }\n\n getEnvironmentBoundToken(tokenStorage: AuthTokenStorage): string | null {\n const token = tokenStorage.get();\n if (!token) return null;\n\n if (!this.get()) {\n tokenStorage.clear();\n return null;\n }\n\n return token;\n }\n\n commitLogin(params: CommitLoginSessionParams, tokenStorage: AuthTokenStorage): void {\n tokenStorage.set(params.accessToken, params.tokenOptions);\n this.#session.set(\n {\n environmentFingerprint: this.#environmentFingerprint,\n provider: params.provider,\n loginMethod: params.loginMethod,\n primaryWallet: params.primaryWallet,\n smartAccount: params.smartAccount,\n activeAccount: {\n accountId: params.activeAccount?.accountId ?? params.accountId,\n isMain: params.activeAccount?.isMain ?? true,\n mainAccountId: params.accountId,\n smartAccountAddress: params.activeAccount?.smartAccountAddress,\n label: params.activeAccount?.label,\n },\n username: params.username,\n },\n { maxAgeSeconds: params.tokenOptions.maxAgeSeconds },\n );\n }\n\n ensureSession(params: EnsureSessionParams, options?: SessionCookieOptions): SessionData {\n const existingSession = this.get();\n if (existingSession) {\n const username = params.username || undefined;\n if (username === undefined || username === existingSession.username) {\n return existingSession;\n }\n\n const session = { ...existingSession, username };\n this.#session.set(session, options);\n return session;\n }\n\n const session: SessionData = {\n environmentFingerprint: this.#environmentFingerprint,\n provider: params.provider,\n loginMethod: params.loginMethod,\n primaryWallet: params.primaryWallet,\n smartAccount: params.smartAccount,\n activeAccount: {\n accountId: params.accountId,\n isMain: true,\n mainAccountId: params.accountId,\n },\n username: params.username,\n };\n this.#session.set(session, options);\n return session;\n }\n\n setUsername(username: string | null, options?: SessionCookieOptions): void {\n const session = this.get();\n if (!session) return;\n\n this.#session.set({ ...session, username: username || undefined }, options);\n }\n\n setActiveAccount(\n activeAccount: Omit<ActiveAccountInfo, \"mainAccountId\">,\n options?: SessionCookieOptions,\n ): void {\n const session = this.get();\n if (!session) return;\n\n const mainAccountId = session.activeAccount?.mainAccountId ?? activeAccount.accountId;\n const smartAccountAddress = activeAccount.isMain\n ? session.smartAccount\n : activeAccount.smartAccountAddress;\n const label = activeAccount.isMain ? undefined : activeAccount.label;\n this.#session.set(\n {\n ...session,\n activeAccount: { ...activeAccount, mainAccountId, smartAccountAddress, label },\n username: session.username,\n },\n options,\n );\n }\n\n clear(): void {\n this.#session.clear();\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"session.js","names":["#lastRawValue","#lastDecoded","#environmentFingerprint","#session"],"sources":["../../../src/services/auth/session.ts"],"sourcesContent":["import {\n setCookie,\n deleteCookie,\n getCookie,\n getCookieValue,\n shouldSecureCookies,\n type CookieGetter,\n} from \"../../utils/cookies.js\";\nimport {\n POLYESTER_AUTH_TOKEN_COOKIE_NAME,\n POLYESTER_SESSION_COOKIE_NAME,\n POLYESTER_LOGIN_COOKIE_MAX_AGE,\n} from \"./cookie-constants.js\";\nimport { SessionCodec } from \"./session-codec.js\";\nimport type { PolyesterEnvironment } from \"../../environment.js\";\nimport type { AuthTokenStorage, AuthTokenStorageSetOptions } from \"./token-storage.js\";\nimport type {\n ActiveAccountInfo,\n AuthLoginMethod,\n ServerSessionSnapshot,\n SessionData,\n} from \"./session.types.js\";\n\nexport type { ActiveAccountInfo, AuthLoginMethod, ServerSessionSnapshot, SessionData };\n\nexport interface SessionCookieOptions {\n maxAgeSeconds?: number | null;\n}\n\nexport interface CommitLoginSessionParams {\n accessToken: string;\n tokenOptions: AuthTokenStorageSetOptions;\n provider: SessionData[\"provider\"];\n loginMethod: AuthLoginMethod | null;\n primaryWallet: string;\n smartAccount: string;\n accountId: string;\n activeAccount?: ActiveAccountInfo;\n username?: string;\n}\n\nexport interface EnsureSessionParams {\n provider: SessionData[\"provider\"];\n loginMethod: AuthLoginMethod | null;\n primaryWallet: string;\n smartAccount: string;\n accountId: string;\n username?: string;\n}\n\nfunction resolveSessionCookieMaxAge(options?: SessionCookieOptions): number | undefined {\n if (!options) return POLYESTER_LOGIN_COOKIE_MAX_AGE;\n return options.maxAgeSeconds ?? undefined;\n}\n\n/**\n * Reads and writes the browser display-session cookie.\n */\nclass PolyesterSessionManager {\n /**\n * Replaces the current session state.\n */\n set(session: SessionData, options?: SessionCookieOptions): void {\n setCookie({\n name: POLYESTER_SESSION_COOKIE_NAME,\n value: SessionCodec.encode(session),\n options: {\n path: \"/\",\n maxAge: resolveSessionCookieMaxAge(options),\n secure: shouldSecureCookies(),\n sameSite: \"lax\",\n },\n });\n }\n\n // The session is re-read on every authenticated RPC; memoize the decode\n // (JSON.parse + schema validation) on the raw cookie value.\n #lastRawValue: string | null = null;\n #lastDecoded: SessionData | null = null;\n\n /**\n * Returns the current session state, or null when no session is active.\n */\n get(): SessionData | null {\n const value = getCookie(POLYESTER_SESSION_COOKIE_NAME);\n if (!value) {\n this.#lastRawValue = null;\n this.#lastDecoded = null;\n return null;\n }\n if (value === this.#lastRawValue) return this.#lastDecoded;\n\n const session = SessionCodec.decode(value);\n if (!session) {\n this.clear();\n this.#lastRawValue = null;\n this.#lastDecoded = null;\n return null;\n }\n\n this.#lastRawValue = value;\n this.#lastDecoded = session;\n return session;\n }\n\n /**\n * Clears the current session state.\n */\n clear(): void {\n deleteCookie(POLYESTER_SESSION_COOKIE_NAME);\n }\n}\n\nexport const polyesterSession = new PolyesterSessionManager();\n\nexport function emptyServerSessionSnapshot(): ServerSessionSnapshot {\n return {\n environmentFingerprint: null,\n hasDisplaySession: false,\n provider: null,\n loginMethod: null,\n accountAddresses: null,\n activeAccount: null,\n bearerToken: null,\n username: null,\n };\n}\n\n/**\n * Parses bearer authentication and display-only session cookies into a server snapshot.\n */\nexport function parseServerSessionSnapshot(\n cookies: CookieGetter,\n environment: PolyesterEnvironment,\n): ServerSessionSnapshot {\n const sessionValue = getCookieValue(cookies, POLYESTER_SESSION_COOKIE_NAME);\n const bearerToken = getCookieValue(cookies, POLYESTER_AUTH_TOKEN_COOKIE_NAME) ?? null;\n const session = sessionValue ? SessionCodec.decode(sessionValue) : null;\n\n if (!session) {\n return { ...emptyServerSessionSnapshot(), bearerToken };\n }\n\n if (session.environmentFingerprint !== environment.fingerprint) {\n return emptyServerSessionSnapshot();\n }\n\n return {\n environmentFingerprint: session.environmentFingerprint,\n hasDisplaySession: true,\n provider: session.provider,\n loginMethod: session.loginMethod,\n accountAddresses: {\n ownerAddress: session.primaryWallet,\n accountAddress: session.smartAccount,\n },\n activeAccount: session.activeAccount ?? null,\n bearerToken,\n username: session.username ?? null,\n };\n}\n\n/**\n * Owns environment-bound auth session transitions for browser clients.\n */\nexport class AuthSessionStore {\n #environmentFingerprint: string;\n #session: PolyesterSessionManager;\n\n constructor({ environmentFingerprint }: { environmentFingerprint: string }) {\n this.#environmentFingerprint = environmentFingerprint;\n this.#session = polyesterSession;\n }\n\n get(): SessionData | null {\n const session = this.#session.get();\n if (!session) return null;\n if (session.environmentFingerprint !== this.#environmentFingerprint) {\n this.#session.clear();\n return null;\n }\n return session;\n }\n\n getEnvironmentBoundToken(tokenStorage: AuthTokenStorage): string | null {\n const token = tokenStorage.get();\n if (!token) return null;\n\n const session = this.#session.get();\n if (!session) return token;\n\n if (session.environmentFingerprint !== this.#environmentFingerprint) {\n this.#session.clear();\n tokenStorage.clear();\n return null;\n }\n\n return token;\n }\n\n commitLogin(params: CommitLoginSessionParams, tokenStorage: AuthTokenStorage): void {\n tokenStorage.set(params.accessToken, params.tokenOptions);\n this.#session.set(\n {\n environmentFingerprint: this.#environmentFingerprint,\n provider: params.provider,\n loginMethod: params.loginMethod,\n primaryWallet: params.primaryWallet,\n smartAccount: params.smartAccount,\n activeAccount: {\n accountId: params.activeAccount?.accountId ?? params.accountId,\n isMain: params.activeAccount?.isMain ?? true,\n mainAccountId: params.accountId,\n smartAccountAddress: params.activeAccount?.smartAccountAddress,\n label: params.activeAccount?.label,\n },\n username: params.username,\n },\n { maxAgeSeconds: params.tokenOptions.maxAgeSeconds },\n );\n }\n\n ensureSession(params: EnsureSessionParams, options?: SessionCookieOptions): SessionData {\n const existingSession = this.get();\n if (existingSession) {\n const username = params.username || undefined;\n if (username === undefined || username === existingSession.username) {\n return existingSession;\n }\n\n const session = { ...existingSession, username };\n this.#session.set(session, options);\n return session;\n }\n\n const session: SessionData = {\n environmentFingerprint: this.#environmentFingerprint,\n provider: params.provider,\n loginMethod: params.loginMethod,\n primaryWallet: params.primaryWallet,\n smartAccount: params.smartAccount,\n activeAccount: {\n accountId: params.accountId,\n isMain: true,\n mainAccountId: params.accountId,\n },\n username: params.username,\n };\n this.#session.set(session, options);\n return session;\n }\n\n setUsername(username: string | null, options?: SessionCookieOptions): void {\n const session = this.get();\n if (!session) return;\n\n this.#session.set({ ...session, username: username || undefined }, options);\n }\n\n setActiveAccount(\n activeAccount: Omit<ActiveAccountInfo, \"mainAccountId\">,\n options?: SessionCookieOptions,\n ): void {\n const session = this.get();\n if (!session) return;\n\n const mainAccountId = session.activeAccount?.mainAccountId ?? activeAccount.accountId;\n const smartAccountAddress = activeAccount.isMain\n ? session.smartAccount\n : activeAccount.smartAccountAddress;\n const label = activeAccount.isMain ? undefined : activeAccount.label;\n this.#session.set(\n {\n ...session,\n activeAccount: { ...activeAccount, mainAccountId, smartAccountAddress, label },\n username: session.username,\n },\n options,\n );\n }\n\n clear(): void {\n this.#session.clear();\n }\n}\n"],"mappings":"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"}
@@ -11,10 +11,10 @@ function createAuthTokenStorageSetOptions(token) {
11
11
  expiresAt: null,
12
12
  maxAgeSeconds: null
13
13
  };
14
- const nowSeconds = Math.floor(Date.now() / 1e3);
14
+ const nowSeconds = Date.now() / 1e3;
15
15
  return {
16
16
  expiresAt: /* @__PURE__ */ new Date(exp * 1e3),
17
- maxAgeSeconds: Math.max(0, exp - nowSeconds)
17
+ maxAgeSeconds: Math.max(0, Math.floor(exp - nowSeconds))
18
18
  };
19
19
  }
20
20
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"token-storage.js","names":[],"sources":["../../../src/services/auth/token-storage.ts"],"sourcesContent":["import { CookieManager } from \"../../utils/cookies.js\";\nimport { getJwtExpiration } from \"../../utils/jwt.js\";\nimport { POLYESTER_AUTH_TOKEN_COOKIE_NAME } from \"./cookie-constants.js\";\n\nexport interface AuthTokenStorageSetOptions {\n expiresAt: Date | null;\n maxAgeSeconds: number | null;\n}\n\nexport interface AuthTokenStorage {\n get(): string | null;\n set(token: string, options: AuthTokenStorageSetOptions): void;\n clear(): void;\n}\n\nexport interface CookieAuthTokenStorageOptions {\n cookieName?: string;\n path?: string;\n secure?: boolean;\n sameSite?: \"lax\" | \"strict\" | \"none\";\n}\n\n/**\n * Builds cookie write options for auth token storage.\n */\nexport function createAuthTokenStorageSetOptions(token: string): AuthTokenStorageSetOptions {\n const exp = getJwtExpiration(token);\n if (exp === null) {\n return { expiresAt: null, maxAgeSeconds: null };\n }\n\n const nowSeconds = Math.floor(Date.now() / 1000);\n return {\n expiresAt: new Date(exp * 1000),\n maxAgeSeconds: Math.max(0, exp - nowSeconds),\n };\n}\n\n/**\n * Creates in-memory auth token storage.\n */\nexport function createMemoryAuthTokenStorage(initialToken?: string | null): AuthTokenStorage {\n let storedToken = initialToken ?? null;\n\n return {\n get: () => storedToken,\n set: (token: string) => {\n storedToken = token;\n },\n clear: () => {\n storedToken = null;\n },\n };\n}\n\n/**\n * Creates cookie-backed auth token storage.\n */\nexport function createCookieAuthTokenStorage(\n options: CookieAuthTokenStorageOptions = {},\n): AuthTokenStorage {\n const cookieName = options.cookieName ?? POLYESTER_AUTH_TOKEN_COOKIE_NAME;\n const path = options.path ?? \"/\";\n const cookie = new CookieManager({ name: cookieName });\n\n return {\n get: () => cookie.get(),\n set: (token: string, setOptions: AuthTokenStorageSetOptions) => {\n cookie.set(token, {\n path,\n secure: options.secure,\n sameSite: options.sameSite,\n maxAge: setOptions.maxAgeSeconds ?? undefined,\n expires: setOptions.expiresAt ?? undefined,\n });\n },\n clear: () => {\n cookie.clear({ path });\n },\n };\n}\n"],"mappings":";;;;;;;AAyBA,SAAgB,iCAAiC,OAA2C;CACxF,MAAM,MAAM,iBAAiB,KAAK;CAClC,IAAI,QAAQ,MACR,OAAO;EAAE,WAAW;EAAM,eAAe;CAAK;CAGlD,MAAM,aAAa,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;CAC/C,OAAO;EACH,2BAAW,IAAI,KAAK,MAAM,GAAI;EAC9B,eAAe,KAAK,IAAI,GAAG,MAAM,UAAU;CAC/C;AACJ;;;;AAKA,SAAgB,6BAA6B,cAAgD;CACzF,IAAI,cAAc,gBAAgB;CAElC,OAAO;EACH,WAAW;EACX,MAAM,UAAkB;GACpB,cAAc;EAClB;EACA,aAAa;GACT,cAAc;EAClB;CACJ;AACJ;;;;AAKA,SAAgB,6BACZ,UAAyC,CAAC,GAC1B;CAChB,MAAM,aAAa,QAAQ,cAAA;CAC3B,MAAM,OAAO,QAAQ,QAAQ;CAC7B,MAAM,SAAS,IAAI,cAAc,EAAE,MAAM,WAAW,CAAC;CAErD,OAAO;EACH,WAAW,OAAO,IAAI;EACtB,MAAM,OAAe,eAA2C;GAC5D,OAAO,IAAI,OAAO;IACd;IACA,QAAQ,QAAQ;IAChB,UAAU,QAAQ;IAClB,QAAQ,WAAW,iBAAiB,KAAA;IACpC,SAAS,WAAW,aAAa,KAAA;GACrC,CAAC;EACL;EACA,aAAa;GACT,OAAO,MAAM,EAAE,KAAK,CAAC;EACzB;CACJ;AACJ"}
1
+ {"version":3,"file":"token-storage.js","names":[],"sources":["../../../src/services/auth/token-storage.ts"],"sourcesContent":["import { CookieManager } from \"../../utils/cookies.js\";\nimport { getJwtExpiration } from \"../../utils/jwt.js\";\nimport { POLYESTER_AUTH_TOKEN_COOKIE_NAME } from \"./cookie-constants.js\";\n\nexport interface AuthTokenStorageSetOptions {\n expiresAt: Date | null;\n maxAgeSeconds: number | null;\n}\n\nexport interface AuthTokenStorage {\n get(): string | null;\n set(token: string, options: AuthTokenStorageSetOptions): void;\n clear(): void;\n}\n\nexport interface CookieAuthTokenStorageOptions {\n cookieName?: string;\n path?: string;\n secure?: boolean;\n sameSite?: \"lax\" | \"strict\" | \"none\";\n}\n\n/**\n * Builds cookie write options for auth token storage.\n */\nexport function createAuthTokenStorageSetOptions(token: string): AuthTokenStorageSetOptions {\n const exp = getJwtExpiration(token);\n if (exp === null) {\n return { expiresAt: null, maxAgeSeconds: null };\n }\n\n const nowSeconds = Date.now() / 1000;\n return {\n expiresAt: new Date(exp * 1000),\n maxAgeSeconds: Math.max(0, Math.floor(exp - nowSeconds)),\n };\n}\n\n/**\n * Creates in-memory auth token storage.\n */\nexport function createMemoryAuthTokenStorage(initialToken?: string | null): AuthTokenStorage {\n let storedToken = initialToken ?? null;\n\n return {\n get: () => storedToken,\n set: (token: string) => {\n storedToken = token;\n },\n clear: () => {\n storedToken = null;\n },\n };\n}\n\n/**\n * Creates cookie-backed auth token storage.\n */\nexport function createCookieAuthTokenStorage(\n options: CookieAuthTokenStorageOptions = {},\n): AuthTokenStorage {\n const cookieName = options.cookieName ?? POLYESTER_AUTH_TOKEN_COOKIE_NAME;\n const path = options.path ?? \"/\";\n const cookie = new CookieManager({ name: cookieName });\n\n return {\n get: () => cookie.get(),\n set: (token: string, setOptions: AuthTokenStorageSetOptions) => {\n cookie.set(token, {\n path,\n secure: options.secure,\n sameSite: options.sameSite,\n maxAge: setOptions.maxAgeSeconds ?? undefined,\n expires: setOptions.expiresAt ?? undefined,\n });\n },\n clear: () => {\n cookie.clear({ path });\n },\n };\n}\n"],"mappings":";;;;;;;AAyBA,SAAgB,iCAAiC,OAA2C;CACxF,MAAM,MAAM,iBAAiB,KAAK;CAClC,IAAI,QAAQ,MACR,OAAO;EAAE,WAAW;EAAM,eAAe;CAAK;CAGlD,MAAM,aAAa,KAAK,IAAI,IAAI;CAChC,OAAO;EACH,2BAAW,IAAI,KAAK,MAAM,GAAI;EAC9B,eAAe,KAAK,IAAI,GAAG,KAAK,MAAM,MAAM,UAAU,CAAC;CAC3D;AACJ;;;;AAKA,SAAgB,6BAA6B,cAAgD;CACzF,IAAI,cAAc,gBAAgB;CAElC,OAAO;EACH,WAAW;EACX,MAAM,UAAkB;GACpB,cAAc;EAClB;EACA,aAAa;GACT,cAAc;EAClB;CACJ;AACJ;;;;AAKA,SAAgB,6BACZ,UAAyC,CAAC,GAC1B;CAChB,MAAM,aAAa,QAAQ,cAAA;CAC3B,MAAM,OAAO,QAAQ,QAAQ;CAC7B,MAAM,SAAS,IAAI,cAAc,EAAE,MAAM,WAAW,CAAC;CAErD,OAAO;EACH,WAAW,OAAO,IAAI;EACtB,MAAM,OAAe,eAA2C;GAC5D,OAAO,IAAI,OAAO;IACd;IACA,QAAQ,QAAQ;IAChB,UAAU,QAAQ;IAClB,QAAQ,WAAW,iBAAiB,KAAA;IACpC,SAAS,WAAW,aAAa,KAAA;GACrC,CAAC;EACL;EACA,aAAa;GACT,OAAO,MAAM,EAAE,KAAK,CAAC;EACzB;CACJ;AACJ"}