@lunora/client 1.0.0-alpha.3 → 1.0.0-alpha.4

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.
@@ -1,4 +1,4 @@
1
- import { U as User, L as LunoraClient } from "../packem_shared/lunora-client.d-DGvyuJ_p.mjs";
1
+ import { U as User, L as LunoraClient } from "../packem_shared/lunora-client.d-CbnxmuqG.mjs";
2
2
  import '@lunora/runtime';
3
3
  interface IdentityStore {
4
4
  /** Read the current resolved user, or `null` when signed out / not yet resolved. */
@@ -1,4 +1,4 @@
1
- import { U as User, L as LunoraClient } from "../packem_shared/lunora-client.d-DGvyuJ_p.js";
1
+ import { U as User, L as LunoraClient } from "../packem_shared/lunora-client.d-CbnxmuqG.js";
2
2
  import '@lunora/runtime';
3
3
  interface IdentityStore {
4
4
  /** Read the current resolved user, or `null` when signed out / not yet resolved. */
package/dist/index.d.mts CHANGED
@@ -1,6 +1,6 @@
1
- import { c as PersistenceAdapter, B as BookmarkStorage, F as FunctionReference, A as ArgsOf, M as MutationCallOptions, R as ReturnOf, O as OfflineQueueOptions, Q as QueryCacheAdapter, d as ReconnectOptions } from "./packem_shared/lunora-client.d-DGvyuJ_p.mjs";
2
- export { type C as CachedQuery, type e as ClientMessage, type f as ConnectionStatus, D as DEFAULT_MAX_BUFFER, type g as FunctionArgumentDescriptor, type h as FunctionDescriptor, type G as GlobalFacetResult, type i as GlobalFacetValue, type j as GlobalFilterClause, type k as GlobalTableInfo, type l as GlobalTablePage, L as LunoraClient, type m as LunoraClientOptions, type n as OptimisticLocalStore, type o as OptimisticUpdate, type p as PersistedMutation, type P as Preloaded, type q as RpcEnvelope, type r as RpcResponseBody, type s as ScheduleRecord, type t as SchedulerPoolStatus, type u as SchedulerStatus, type v as ServerMessage, type w as ShardTrafficEntry, type x as ShardTrafficResult, type y as StorageListPage, type z as StorageObject, type E as StreamHandle, type H as StreamIterable, type I as SubscriptionCallback, type S as SubscriptionError, type b as SubscriptionErrorCallback, J as SubscriptionRegistry, type K as SubscriptionState, type a as Unsubscribe, type U as User, type W as WorkflowInstanceAction, type N as WorkflowInstanceDetail, type T as WorkflowInstancePage, type V as WorkflowInstanceStatus, type X as WorkflowInstanceSummary, type Y as WorkflowStepDetail, Z as createLocalStore, _ as createStream } from "./packem_shared/lunora-client.d-DGvyuJ_p.mjs";
3
- export { p as preloadQuery, a as preloadedQueryResult } from "./packem_shared/preload.d-dSaRMuhL.mjs";
1
+ import { c as PersistenceAdapter, B as BookmarkStorage, F as FunctionReference, A as ArgsOf, M as MutationCallOptions, R as ReturnOf, O as OfflineQueueOptions, Q as QueryCacheAdapter, d as ReconnectOptions } from "./packem_shared/lunora-client.d-CbnxmuqG.mjs";
2
+ export { type C as CachedQuery, type e as ClientMessage, type f as ConnectionStatus, D as DEFAULT_MAX_BUFFER, type g as FunctionArgumentDescriptor, type h as FunctionDescriptor, type G as GlobalFacetResult, type i as GlobalFacetValue, type j as GlobalFilterClause, type k as GlobalTableInfo, type l as GlobalTablePage, L as LunoraClient, type m as LunoraClientOptions, type n as OptimisticLocalStore, type o as OptimisticUpdate, type p as PersistedMutation, type P as Preloaded, type q as RpcEnvelope, type r as RpcResponseBody, type s as ScheduleRecord, type t as SchedulerPoolStatus, type u as SchedulerStatus, type v as ServerMessage, type w as ShardTrafficEntry, type x as ShardTrafficResult, type y as StorageListPage, type z as StorageObject, type E as StreamHandle, type H as StreamIterable, type I as SubscriptionCallback, type S as SubscriptionError, type b as SubscriptionErrorCallback, J as SubscriptionRegistry, type K as SubscriptionState, type a as Unsubscribe, type U as User, type W as WorkflowInstanceAction, type N as WorkflowInstanceDetail, type T as WorkflowInstancePage, type V as WorkflowInstanceStatus, type X as WorkflowInstanceSummary, type Y as WorkflowStepDetail, Z as createLocalStore, _ as createStream } from "./packem_shared/lunora-client.d-CbnxmuqG.mjs";
3
+ export { p as preloadQuery, a as preloadedQueryResult } from "./packem_shared/preload.d-Bhq6DBSe.mjs";
4
4
  export type { AuthCapabilities, AuthImpersonation, AuthPage, AuthSession, AuthUser, CronJobInfo, VectorIndexSummary, VectorQueryMatch } from '@lunora/runtime';
5
5
  /**
6
6
  * The slice of React Native's `AsyncStorage` (or any async key/value store —
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { c as PersistenceAdapter, B as BookmarkStorage, F as FunctionReference, A as ArgsOf, M as MutationCallOptions, R as ReturnOf, O as OfflineQueueOptions, Q as QueryCacheAdapter, d as ReconnectOptions } from "./packem_shared/lunora-client.d-DGvyuJ_p.js";
2
- export { type C as CachedQuery, type e as ClientMessage, type f as ConnectionStatus, D as DEFAULT_MAX_BUFFER, type g as FunctionArgumentDescriptor, type h as FunctionDescriptor, type G as GlobalFacetResult, type i as GlobalFacetValue, type j as GlobalFilterClause, type k as GlobalTableInfo, type l as GlobalTablePage, L as LunoraClient, type m as LunoraClientOptions, type n as OptimisticLocalStore, type o as OptimisticUpdate, type p as PersistedMutation, type P as Preloaded, type q as RpcEnvelope, type r as RpcResponseBody, type s as ScheduleRecord, type t as SchedulerPoolStatus, type u as SchedulerStatus, type v as ServerMessage, type w as ShardTrafficEntry, type x as ShardTrafficResult, type y as StorageListPage, type z as StorageObject, type E as StreamHandle, type H as StreamIterable, type I as SubscriptionCallback, type S as SubscriptionError, type b as SubscriptionErrorCallback, J as SubscriptionRegistry, type K as SubscriptionState, type a as Unsubscribe, type U as User, type W as WorkflowInstanceAction, type N as WorkflowInstanceDetail, type T as WorkflowInstancePage, type V as WorkflowInstanceStatus, type X as WorkflowInstanceSummary, type Y as WorkflowStepDetail, Z as createLocalStore, _ as createStream } from "./packem_shared/lunora-client.d-DGvyuJ_p.js";
3
- export { p as preloadQuery, a as preloadedQueryResult } from "./packem_shared/preload.d-BoDmFqSG.js";
1
+ import { c as PersistenceAdapter, B as BookmarkStorage, F as FunctionReference, A as ArgsOf, M as MutationCallOptions, R as ReturnOf, O as OfflineQueueOptions, Q as QueryCacheAdapter, d as ReconnectOptions } from "./packem_shared/lunora-client.d-CbnxmuqG.js";
2
+ export { type C as CachedQuery, type e as ClientMessage, type f as ConnectionStatus, D as DEFAULT_MAX_BUFFER, type g as FunctionArgumentDescriptor, type h as FunctionDescriptor, type G as GlobalFacetResult, type i as GlobalFacetValue, type j as GlobalFilterClause, type k as GlobalTableInfo, type l as GlobalTablePage, L as LunoraClient, type m as LunoraClientOptions, type n as OptimisticLocalStore, type o as OptimisticUpdate, type p as PersistedMutation, type P as Preloaded, type q as RpcEnvelope, type r as RpcResponseBody, type s as ScheduleRecord, type t as SchedulerPoolStatus, type u as SchedulerStatus, type v as ServerMessage, type w as ShardTrafficEntry, type x as ShardTrafficResult, type y as StorageListPage, type z as StorageObject, type E as StreamHandle, type H as StreamIterable, type I as SubscriptionCallback, type S as SubscriptionError, type b as SubscriptionErrorCallback, J as SubscriptionRegistry, type K as SubscriptionState, type a as Unsubscribe, type U as User, type W as WorkflowInstanceAction, type N as WorkflowInstanceDetail, type T as WorkflowInstancePage, type V as WorkflowInstanceStatus, type X as WorkflowInstanceSummary, type Y as WorkflowStepDetail, Z as createLocalStore, _ as createStream } from "./packem_shared/lunora-client.d-CbnxmuqG.js";
3
+ export { p as preloadQuery, a as preloadedQueryResult } from "./packem_shared/preload.d-C73DT6bL.js";
4
4
  export type { AuthCapabilities, AuthImpersonation, AuthPage, AuthSession, AuthUser, CronJobInfo, VectorIndexSummary, VectorQueryMatch } from '@lunora/runtime';
5
5
  /**
6
6
  * The slice of React Native's `AsyncStorage` (or any async key/value store —
package/dist/index.mjs CHANGED
@@ -3,7 +3,7 @@ export { default as createInMemoryBookmarkStorage } from './packem_shared/create
3
3
  export { applyDelta, isMutationDelta } from './packem_shared/applyDelta-4jFGTPA3.mjs';
4
4
  export { CONFLICT_ERROR_CODE, isConflictError } from './packem_shared/CONFLICT_ERROR_CODE-aUdVbEDw.mjs';
5
5
  export { createLocalStore } from './packem_shared/createLocalStore-DSUfoLqY.mjs';
6
- export { LunoraClient } from './packem_shared/LunoraClient-UiULzH_1.mjs';
6
+ export { LunoraClient } from './packem_shared/LunoraClient-DHMV-94j.mjs';
7
7
  export { createMutationRunner } from './packem_shared/createMutationRunner-BqsavzvG.mjs';
8
8
  export { OfflineQueue } from './packem_shared/OfflineQueue-D5p_QgF_.mjs';
9
9
  export { createInMemoryPersistence, createIndexedDbPersistence } from './packem_shared/createInMemoryPersistence-CW82inU5.mjs';
@@ -11,4 +11,4 @@ export { preloadQuery, preloadedQueryResult } from './packem_shared/preloadQuery
11
11
  export { createInMemoryQueryCache, createIndexedDbQueryCache, queryCacheKey } from './packem_shared/createInMemoryQueryCache-B1PQ9Twl.mjs';
12
12
  export { createReconnect } from './packem_shared/createReconnect-Di_-oHH7.mjs';
13
13
  export { DEFAULT_MAX_BUFFER, createStream } from './packem_shared/DEFAULT_MAX_BUFFER-BDkqO5PW.mjs';
14
- export { SubscriptionRegistry } from './packem_shared/SubscriptionRegistry-B-Qx_Gux.mjs';
14
+ export { S as SubscriptionRegistry } from './packem_shared/subscription-C1Jy7HiF.mjs';
@@ -1,3 +1,4 @@
1
+ import { S as SubscriptionRegistry, s as stableStringify } from './subscription-C1Jy7HiF.mjs';
1
2
  import createInMemoryBookmarkStorage from './createInMemoryBookmarkStorage-BoN7a7TH.mjs';
2
3
  import { isMutationDelta, applyDelta } from './applyDelta-4jFGTPA3.mjs';
3
4
  import { createLocalStore } from './createLocalStore-DSUfoLqY.mjs';
@@ -5,13 +6,13 @@ import { OfflineQueue, nextId, reportPersistenceError } from './OfflineQueue-D5p
5
6
  import { queryCacheKey } from './createInMemoryQueryCache-B1PQ9Twl.mjs';
6
7
  import { createReconnect } from './createReconnect-Di_-oHH7.mjs';
7
8
  import { createStream } from './DEFAULT_MAX_BUFFER-BDkqO5PW.mjs';
8
- import { SubscriptionRegistry } from './SubscriptionRegistry-B-Qx_Gux.mjs';
9
9
 
10
10
  const RPC_PATH = "/_lunora/rpc";
11
11
  const WS_PATH = "/_lunora/ws";
12
12
  const bucketQuery = (bucket) => bucket === void 0 || bucket === "" ? "" : `&bucket=${encodeURIComponent(bucket)}`;
13
13
  const WS_KEEPALIVE_PING = "lunora-ping";
14
14
  const DEFAULT_HEARTBEAT_INTERVAL_MS = 3e4;
15
+ const DEFAULT_CONNECT_TIMEOUT_MS = 1e4;
15
16
  const QUERY_CACHE_DEBOUNCE_MS = 250;
16
17
  const MAX_PENDING_STREAMS = 64;
17
18
  const SHARD_TRAFFIC_PATH = "/_lunora/admin/shard-traffic";
@@ -63,22 +64,6 @@ const AUTH_REMOVE_MEMBER_PATH = "/_lunora/admin/auth/organizations/members/remov
63
64
  const AUTH_CANCEL_INVITATION_PATH = "/_lunora/admin/auth/organizations/invitations/cancel";
64
65
  const DEFAULT_AUTH_BASE_PATH = "/api/auth";
65
66
  const GET_SESSION_PATH = "/get-session";
66
- const compareEntryKeys = ([a], [b]) => {
67
- if (a < b) {
68
- return -1;
69
- }
70
- return a > b ? 1 : 0;
71
- };
72
- const stableStringify = (value) => {
73
- if (value === null || typeof value !== "object") {
74
- return JSON.stringify(value);
75
- }
76
- if (Array.isArray(value)) {
77
- return `[${value.map((entry) => stableStringify(entry)).join(",")}]`;
78
- }
79
- const entries = Object.entries(value).toSorted(compareEntryKeys);
80
- return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${stableStringify(v)}`).join(",")}}`;
81
- };
82
67
  const deriveWsUrl = (url) => {
83
68
  if (url.startsWith("https://")) {
84
69
  return `wss://${url.slice("https://".length)}`;
@@ -183,6 +168,8 @@ class LunoraClient {
183
168
  WebSocketImpl;
184
169
  bookmark;
185
170
  reconnectOptions;
171
+ /** WS connect timeout (ms); `0` disables it. See {@link LunoraClientOptions.connectTimeoutMs}. */
172
+ connectTimeoutMs;
186
173
  /** Keepalive cadence (ms); `0` disables the heartbeat. See {@link LunoraClientOptions.heartbeatIntervalMs}. */
187
174
  heartbeatIntervalMs;
188
175
  offlineQueue;
@@ -276,6 +263,7 @@ class LunoraClient {
276
263
  this.bookmark = options.bookmarkStorage ?? createInMemoryBookmarkStorage();
277
264
  this.reconnectOptions = options.reconnect;
278
265
  this.heartbeatIntervalMs = options.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS;
266
+ this.connectTimeoutMs = options.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS;
279
267
  this.defaultConnectionContext = options.connectionContext;
280
268
  this.persistence = options.persistence;
281
269
  this.queryCache = options.queryCache === false ? void 0 : options.queryCache;
@@ -739,8 +727,12 @@ class LunoraClient {
739
727
  * List a workflow's instances via the admin Workflows proxy
740
728
  * (`/_lunora/admin/workflows/instances`) — the Cloudflare control-plane data
741
729
  * the `Workflow` binding can't expose. Requires the worker to be built with a
742
- * `workflowsClient` (Cloudflare account id + API token); otherwise the proxy
743
- * responds 501 and this rejects. `name` is the deployed workflow name.
730
+ * `workflowsClient` (Cloudflare account id + API token). When one isn't
731
+ * configured this does NOT reject: the proxy returns a `200 { configured:
732
+ * false }` sentinel, so the result resolves with `configured === false` and an
733
+ * empty `instances` list — callers should branch on that flag rather than
734
+ * try/catch. (The instance-detail / status endpoints still reject with 501.)
735
+ * `name` is the deployed workflow name.
744
736
  */
745
737
  async listWorkflowInstances(options) {
746
738
  if (this.closed) {
@@ -757,7 +749,13 @@ class LunoraClient {
757
749
  query.set("perPage", String(options.perPage));
758
750
  }
759
751
  const body = await this.adminFetch(`${WORKFLOWS_INSTANCES_PATH}?${query.toString()}`, "GET");
760
- return { instances: body.instances ?? [], page: body.page ?? 1, perPage: body.perPage ?? options.perPage ?? 0, totalCount: body.totalCount };
752
+ return {
753
+ configured: body.configured,
754
+ instances: body.instances ?? [],
755
+ page: body.page ?? 1,
756
+ perPage: body.perPage ?? options.perPage ?? 0,
757
+ totalCount: body.totalCount
758
+ };
761
759
  }
762
760
  /** Read one workflow instance with its step timeline (`/_lunora/admin/workflows/instance`). */
763
761
  async getWorkflowInstance(options) {
@@ -1375,6 +1373,10 @@ class LunoraClient {
1375
1373
  clearTimeout(conn.reconnectTimer);
1376
1374
  conn.reconnectTimer = void 0;
1377
1375
  }
1376
+ if (conn.connectTimer !== void 0) {
1377
+ clearTimeout(conn.connectTimer);
1378
+ conn.connectTimer = void 0;
1379
+ }
1378
1380
  this.stopHeartbeat(conn);
1379
1381
  if (conn.socket) {
1380
1382
  try {
@@ -1564,6 +1566,7 @@ class LunoraClient {
1564
1566
  let conn = this.connections.get(key);
1565
1567
  if (!conn) {
1566
1568
  conn = {
1569
+ connectTimer: void 0,
1567
1570
  heartbeatTimer: void 0,
1568
1571
  pendingUnsubscribes: [],
1569
1572
  reconnect: createReconnect(this.reconnectOptions),
@@ -1754,7 +1757,27 @@ class LunoraClient {
1754
1757
  this.emitConnectionStatus();
1755
1758
  const socket = new this.WebSocketImpl(this.wsUrlFor(shardKey));
1756
1759
  conn.socket = socket;
1760
+ if (this.connectTimeoutMs > 0) {
1761
+ conn.connectTimer = setTimeout(() => {
1762
+ conn.connectTimer = void 0;
1763
+ if (conn.socket !== socket || conn.wsState !== "connecting") {
1764
+ return;
1765
+ }
1766
+ try {
1767
+ socket.close();
1768
+ } catch {
1769
+ }
1770
+ this.handleDisconnect(conn);
1771
+ }, this.connectTimeoutMs);
1772
+ }
1757
1773
  socket.addEventListener("open", () => {
1774
+ if (conn.socket !== socket) {
1775
+ return;
1776
+ }
1777
+ if (conn.connectTimer !== void 0) {
1778
+ clearTimeout(conn.connectTimer);
1779
+ conn.connectTimer = void 0;
1780
+ }
1758
1781
  conn.wsState = "open";
1759
1782
  conn.wasEverConnected = true;
1760
1783
  conn.reconnect.reset();
@@ -1793,12 +1816,18 @@ class LunoraClient {
1793
1816
  this.handleServerMessage(event.data, shardKey);
1794
1817
  });
1795
1818
  socket.addEventListener("close", (event) => {
1819
+ if (conn.socket !== socket) {
1820
+ return;
1821
+ }
1796
1822
  if (event?.code === 4001) {
1797
1823
  this.notifyTokenExpired();
1798
1824
  }
1799
1825
  this.handleDisconnect(conn);
1800
1826
  });
1801
1827
  socket.addEventListener("error", () => {
1828
+ if (conn.socket !== socket) {
1829
+ return;
1830
+ }
1802
1831
  if (conn.wsState === "connecting" || conn.wsState === "open") {
1803
1832
  this.handleDisconnect(conn);
1804
1833
  }
@@ -1812,6 +1841,10 @@ class LunoraClient {
1812
1841
  return;
1813
1842
  }
1814
1843
  this.stopHeartbeat(conn);
1844
+ if (conn.connectTimer !== void 0) {
1845
+ clearTimeout(conn.connectTimer);
1846
+ conn.connectTimer = void 0;
1847
+ }
1815
1848
  conn.socket = void 0;
1816
1849
  conn.wsState = "idle";
1817
1850
  this.emitConnectionStatus();
@@ -0,0 +1 @@
1
+ export { S as SubscriptionRegistry } from './subscription-C1Jy7HiF.mjs';
@@ -1,4 +1,4 @@
1
- import { LunoraClient } from './LunoraClient-UiULzH_1.mjs';
1
+ import { LunoraClient } from './LunoraClient-DHMV-94j.mjs';
2
2
 
3
3
  const createServerClient = (options) => {
4
4
  const client = new LunoraClient({ fetch: options.fetch, url: options.url });
@@ -184,6 +184,16 @@ interface LunoraClientOptions {
184
184
  * hook needs connection context.
185
185
  */
186
186
  connectionContext?: Record<string, unknown>;
187
+ /**
188
+ * Fail-fast timeout (ms) for opening a subscription WebSocket. If the
189
+ * handshake doesn't complete within this window — a hung dev proxy or a cold
190
+ * worker that never upgrades — the client force-closes the socket and routes
191
+ * through its normal reconnect/backoff (surfacing `offline` status) instead
192
+ * of leaving the live channel silently stuck on the browser's much longer
193
+ * default. Does not affect HTTP queries/mutations (those never ride the WS).
194
+ * Defaults to 10000 (10s); set to `0` (or negative) to disable.
195
+ */
196
+ connectTimeoutMs?: number;
187
197
  fetch?: typeof fetch;
188
198
  /**
189
199
  * Interval (ms) between keepalive pings sent on each open subscription
@@ -582,6 +592,13 @@ interface WorkflowInstanceDetail extends WorkflowInstanceSummary {
582
592
  }
583
593
  /** A page of workflow instances. */
584
594
  interface WorkflowInstancePage {
595
+ /**
596
+ * Whether workflow inspection is configured on the worker (a Cloudflare
597
+ * account id + API token). `false` when the admin proxy reports it can't
598
+ * inspect instances; omitted (treated as configured) otherwise. Lets a
599
+ * caller render a "set credentials" state without a failed request.
600
+ */
601
+ configured?: boolean;
585
602
  instances: WorkflowInstanceSummary[];
586
603
  page: number;
587
604
  perPage: number;
@@ -637,8 +654,11 @@ interface SubscriptionState {
637
654
  }
638
655
  /**
639
656
  * Active subscription registry. The client keys subscriptions by
640
- * `(functionPath, JSON.stringify(args), shardKey)` so duplicate calls share a
641
- * single server-side registration.
657
+ * `(functionPath, stableStringify(args), shardKey)` so duplicate calls share a
658
+ * single server-side registration. Args are stably encoded (keys sorted at every
659
+ * depth) so two structurally-equal arg records constructed with a different key
660
+ * order (`{ a, b }` vs `{ b, a }`) collapse to the same key instead of leaking a
661
+ * duplicate subscription.
642
662
  */
643
663
  declare class SubscriptionRegistry {
644
664
  static key(functionPath: string, args: Record<string, unknown>, shardKey?: string): string;
@@ -787,6 +807,8 @@ declare class LunoraClient {
787
807
  private readonly WebSocketImpl;
788
808
  private readonly bookmark;
789
809
  private readonly reconnectOptions;
810
+ /** WS connect timeout (ms); `0` disables it. See {@link LunoraClientOptions.connectTimeoutMs}. */
811
+ private readonly connectTimeoutMs;
790
812
  /** Keepalive cadence (ms); `0` disables the heartbeat. See {@link LunoraClientOptions.heartbeatIntervalMs}. */
791
813
  private readonly heartbeatIntervalMs;
792
814
  private readonly offlineQueue;
@@ -1069,8 +1091,12 @@ declare class LunoraClient {
1069
1091
  * List a workflow's instances via the admin Workflows proxy
1070
1092
  * (`/_lunora/admin/workflows/instances`) — the Cloudflare control-plane data
1071
1093
  * the `Workflow` binding can't expose. Requires the worker to be built with a
1072
- * `workflowsClient` (Cloudflare account id + API token); otherwise the proxy
1073
- * responds 501 and this rejects. `name` is the deployed workflow name.
1094
+ * `workflowsClient` (Cloudflare account id + API token). When one isn't
1095
+ * configured this does NOT reject: the proxy returns a `200 { configured:
1096
+ * false }` sentinel, so the result resolves with `configured === false` and an
1097
+ * empty `instances` list — callers should branch on that flag rather than
1098
+ * try/catch. (The instance-detail / status endpoints still reject with 501.)
1099
+ * `name` is the deployed workflow name.
1074
1100
  */
1075
1101
  listWorkflowInstances(options: {
1076
1102
  name: string;
@@ -184,6 +184,16 @@ interface LunoraClientOptions {
184
184
  * hook needs connection context.
185
185
  */
186
186
  connectionContext?: Record<string, unknown>;
187
+ /**
188
+ * Fail-fast timeout (ms) for opening a subscription WebSocket. If the
189
+ * handshake doesn't complete within this window — a hung dev proxy or a cold
190
+ * worker that never upgrades — the client force-closes the socket and routes
191
+ * through its normal reconnect/backoff (surfacing `offline` status) instead
192
+ * of leaving the live channel silently stuck on the browser's much longer
193
+ * default. Does not affect HTTP queries/mutations (those never ride the WS).
194
+ * Defaults to 10000 (10s); set to `0` (or negative) to disable.
195
+ */
196
+ connectTimeoutMs?: number;
187
197
  fetch?: typeof fetch;
188
198
  /**
189
199
  * Interval (ms) between keepalive pings sent on each open subscription
@@ -582,6 +592,13 @@ interface WorkflowInstanceDetail extends WorkflowInstanceSummary {
582
592
  }
583
593
  /** A page of workflow instances. */
584
594
  interface WorkflowInstancePage {
595
+ /**
596
+ * Whether workflow inspection is configured on the worker (a Cloudflare
597
+ * account id + API token). `false` when the admin proxy reports it can't
598
+ * inspect instances; omitted (treated as configured) otherwise. Lets a
599
+ * caller render a "set credentials" state without a failed request.
600
+ */
601
+ configured?: boolean;
585
602
  instances: WorkflowInstanceSummary[];
586
603
  page: number;
587
604
  perPage: number;
@@ -637,8 +654,11 @@ interface SubscriptionState {
637
654
  }
638
655
  /**
639
656
  * Active subscription registry. The client keys subscriptions by
640
- * `(functionPath, JSON.stringify(args), shardKey)` so duplicate calls share a
641
- * single server-side registration.
657
+ * `(functionPath, stableStringify(args), shardKey)` so duplicate calls share a
658
+ * single server-side registration. Args are stably encoded (keys sorted at every
659
+ * depth) so two structurally-equal arg records constructed with a different key
660
+ * order (`{ a, b }` vs `{ b, a }`) collapse to the same key instead of leaking a
661
+ * duplicate subscription.
642
662
  */
643
663
  declare class SubscriptionRegistry {
644
664
  static key(functionPath: string, args: Record<string, unknown>, shardKey?: string): string;
@@ -787,6 +807,8 @@ declare class LunoraClient {
787
807
  private readonly WebSocketImpl;
788
808
  private readonly bookmark;
789
809
  private readonly reconnectOptions;
810
+ /** WS connect timeout (ms); `0` disables it. See {@link LunoraClientOptions.connectTimeoutMs}. */
811
+ private readonly connectTimeoutMs;
790
812
  /** Keepalive cadence (ms); `0` disables the heartbeat. See {@link LunoraClientOptions.heartbeatIntervalMs}. */
791
813
  private readonly heartbeatIntervalMs;
792
814
  private readonly offlineQueue;
@@ -1069,8 +1091,12 @@ declare class LunoraClient {
1069
1091
  * List a workflow's instances via the admin Workflows proxy
1070
1092
  * (`/_lunora/admin/workflows/instances`) — the Cloudflare control-plane data
1071
1093
  * the `Workflow` binding can't expose. Requires the worker to be built with a
1072
- * `workflowsClient` (Cloudflare account id + API token); otherwise the proxy
1073
- * responds 501 and this rejects. `name` is the deployed workflow name.
1094
+ * `workflowsClient` (Cloudflare account id + API token). When one isn't
1095
+ * configured this does NOT reject: the proxy returns a `200 { configured:
1096
+ * false }` sentinel, so the result resolves with `configured === false` and an
1097
+ * empty `instances` list — callers should branch on that flag rather than
1098
+ * try/catch. (The instance-detail / status endpoints still reject with 501.)
1099
+ * `name` is the deployed workflow name.
1074
1100
  */
1075
1101
  listWorkflowInstances(options: {
1076
1102
  name: string;
@@ -1,4 +1,4 @@
1
- import { F as FunctionReference, L as LunoraClient, A as ArgsOf, R as ReturnOf, P as Preloaded } from "./lunora-client.d-DGvyuJ_p.mjs";
1
+ import { F as FunctionReference, L as LunoraClient, A as ArgsOf, R as ReturnOf, P as Preloaded } from "./lunora-client.d-CbnxmuqG.mjs";
2
2
  /**
3
3
  * Run a query once on the server (during SSR) and capture its result in a
4
4
  * serializable {@link Preloaded} token. Embed the token in the rendered HTML and
@@ -1,4 +1,4 @@
1
- import { F as FunctionReference, L as LunoraClient, A as ArgsOf, R as ReturnOf, P as Preloaded } from "./lunora-client.d-DGvyuJ_p.js";
1
+ import { F as FunctionReference, L as LunoraClient, A as ArgsOf, R as ReturnOf, P as Preloaded } from "./lunora-client.d-CbnxmuqG.js";
2
2
  /**
3
3
  * Run a query once on the server (during SSR) and capture its result in a
4
4
  * serializable {@link Preloaded} token. Embed the token in the rendered HTML and
@@ -0,0 +1,55 @@
1
+ const compareKeys = (a, b) => {
2
+ if (a < b) {
3
+ return -1;
4
+ }
5
+ return a > b ? 1 : 0;
6
+ };
7
+ const stableStringify = (value) => {
8
+ if (value === void 0) {
9
+ return "null";
10
+ }
11
+ if (value === null || typeof value !== "object") {
12
+ return JSON.stringify(value);
13
+ }
14
+ if (Array.isArray(value)) {
15
+ return `[${value.map((item) => stableStringify(item)).join(",")}]`;
16
+ }
17
+ const record = value;
18
+ const keys = Object.keys(record).toSorted(compareKeys);
19
+ const parts = [];
20
+ for (const key of keys) {
21
+ const raw = record[key];
22
+ if (raw === void 0) {
23
+ continue;
24
+ }
25
+ parts.push(`${JSON.stringify(key)}:${stableStringify(raw)}`);
26
+ }
27
+ return `{${parts.join(",")}}`;
28
+ };
29
+
30
+ class SubscriptionRegistry {
31
+ static key(functionPath, args, shardKey) {
32
+ return `${functionPath}::${stableStringify(args)}::${shardKey ?? ""}`;
33
+ }
34
+ byKey = /* @__PURE__ */ new Map();
35
+ byId = /* @__PURE__ */ new Map();
36
+ get(key) {
37
+ return this.byKey.get(key);
38
+ }
39
+ getById(id) {
40
+ return this.byId.get(id);
41
+ }
42
+ add(state) {
43
+ this.byKey.set(SubscriptionRegistry.key(state.fn.__lunoraRef, state.args, state.shardKey), state);
44
+ this.byId.set(state.id, state);
45
+ }
46
+ remove(state) {
47
+ this.byKey.delete(SubscriptionRegistry.key(state.fn.__lunoraRef, state.args, state.shardKey));
48
+ this.byId.delete(state.id);
49
+ }
50
+ all() {
51
+ return [...this.byKey.values()];
52
+ }
53
+ }
54
+
55
+ export { SubscriptionRegistry as S, stableStringify as s };
@@ -1,5 +1,5 @@
1
- import { S as SubscriptionError, F as FunctionReference, A as ArgsOf, R as ReturnOf, L as LunoraClient, a as Unsubscribe } from "../packem_shared/lunora-client.d-DGvyuJ_p.mjs";
2
- export type { b as SubscriptionErrorCallback } from "../packem_shared/lunora-client.d-DGvyuJ_p.mjs";
1
+ import { S as SubscriptionError, F as FunctionReference, A as ArgsOf, R as ReturnOf, L as LunoraClient, a as Unsubscribe } from "../packem_shared/lunora-client.d-CbnxmuqG.mjs";
2
+ export type { b as SubscriptionErrorCallback } from "../packem_shared/lunora-client.d-CbnxmuqG.mjs";
3
3
  import '@lunora/runtime';
4
4
  /**
5
5
  * The sentinel a framework adapter resolves its reactive args to when it wants
@@ -1,5 +1,5 @@
1
- import { S as SubscriptionError, F as FunctionReference, A as ArgsOf, R as ReturnOf, L as LunoraClient, a as Unsubscribe } from "../packem_shared/lunora-client.d-DGvyuJ_p.js";
2
- export type { b as SubscriptionErrorCallback } from "../packem_shared/lunora-client.d-DGvyuJ_p.js";
1
+ import { S as SubscriptionError, F as FunctionReference, A as ArgsOf, R as ReturnOf, L as LunoraClient, a as Unsubscribe } from "../packem_shared/lunora-client.d-CbnxmuqG.js";
2
+ export type { b as SubscriptionErrorCallback } from "../packem_shared/lunora-client.d-CbnxmuqG.js";
3
3
  import '@lunora/runtime';
4
4
  /**
5
5
  * The sentinel a framework adapter resolves its reactive args to when it wants
@@ -1,6 +1,6 @@
1
- import { P as Preloaded, L as LunoraClient } from "../packem_shared/lunora-client.d-DGvyuJ_p.mjs";
2
- export type { A as ArgsOf, F as FunctionReference, R as ReturnOf } from "../packem_shared/lunora-client.d-DGvyuJ_p.mjs";
3
- export { p as preloadQuery, a as preloadedQueryResult } from "../packem_shared/preload.d-dSaRMuhL.mjs";
1
+ import { P as Preloaded, L as LunoraClient } from "../packem_shared/lunora-client.d-CbnxmuqG.mjs";
2
+ export type { A as ArgsOf, F as FunctionReference, R as ReturnOf } from "../packem_shared/lunora-client.d-CbnxmuqG.mjs";
3
+ export { p as preloadQuery, a as preloadedQueryResult } from "../packem_shared/preload.d-Bhq6DBSe.mjs";
4
4
  import '@lunora/runtime';
5
5
  /**
6
6
  * Structural shape of a better-auth `getSession` call's resolved value.
@@ -1,6 +1,6 @@
1
- import { P as Preloaded, L as LunoraClient } from "../packem_shared/lunora-client.d-DGvyuJ_p.js";
2
- export type { A as ArgsOf, F as FunctionReference, R as ReturnOf } from "../packem_shared/lunora-client.d-DGvyuJ_p.js";
3
- export { p as preloadQuery, a as preloadedQueryResult } from "../packem_shared/preload.d-BoDmFqSG.js";
1
+ import { P as Preloaded, L as LunoraClient } from "../packem_shared/lunora-client.d-CbnxmuqG.js";
2
+ export type { A as ArgsOf, F as FunctionReference, R as ReturnOf } from "../packem_shared/lunora-client.d-CbnxmuqG.js";
3
+ export { p as preloadQuery, a as preloadedQueryResult } from "../packem_shared/preload.d-C73DT6bL.js";
4
4
  import '@lunora/runtime';
5
5
  /**
6
6
  * Structural shape of a better-auth `getSession` call's resolved value.
@@ -1,4 +1,4 @@
1
1
  export { getServerSession } from '../packem_shared/getServerSession-8jXewqxd.mjs';
2
2
  export { deserializePreloaded, serializePreloaded } from '../packem_shared/deserializePreloaded-C0eJTY_W.mjs';
3
- export { createServerClient } from '../packem_shared/createServerClient-BjZc3gD8.mjs';
3
+ export { createServerClient } from '../packem_shared/createServerClient-DMh-2UDv.mjs';
4
4
  export { preloadQuery, preloadedQueryResult } from '../packem_shared/preloadQuery-lobFkD2Z.mjs';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/client",
3
- "version": "1.0.0-alpha.3",
3
+ "version": "1.0.0-alpha.4",
4
4
  "description": "Lunora browser SDK: WebSocket transport, optimistic updates, and an offline mutation queue",
5
5
  "keywords": [
6
6
  "cloudflare",
@@ -1,26 +0,0 @@
1
- class SubscriptionRegistry {
2
- static key(functionPath, args, shardKey) {
3
- return `${functionPath}::${JSON.stringify(args)}::${shardKey ?? ""}`;
4
- }
5
- byKey = /* @__PURE__ */ new Map();
6
- byId = /* @__PURE__ */ new Map();
7
- get(key) {
8
- return this.byKey.get(key);
9
- }
10
- getById(id) {
11
- return this.byId.get(id);
12
- }
13
- add(state) {
14
- this.byKey.set(SubscriptionRegistry.key(state.fn.__lunoraRef, state.args, state.shardKey), state);
15
- this.byId.set(state.id, state);
16
- }
17
- remove(state) {
18
- this.byKey.delete(SubscriptionRegistry.key(state.fn.__lunoraRef, state.args, state.shardKey));
19
- this.byId.delete(state.id);
20
- }
21
- all() {
22
- return [...this.byKey.values()];
23
- }
24
- }
25
-
26
- export { SubscriptionRegistry };