reflectdb 0.1.0 → 0.1.2

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 (61) hide show
  1. package/README.md +500 -40
  2. package/dist/cjs/client/index.cjs +32 -17
  3. package/dist/cjs/client/index.d.cts +18 -3
  4. package/dist/cjs/client/storage/indexeddb.d.cts +1 -1
  5. package/dist/cjs/core/index.cjs +28 -28
  6. package/dist/cjs/core/index.d.cts +21 -9
  7. package/dist/cjs/react/index.cjs +48 -33
  8. package/dist/cjs/react/index.d.cts +23 -5
  9. package/dist/cjs/server/drizzle.cjs +2 -2
  10. package/dist/cjs/server/drizzle.d.cts +1 -1
  11. package/dist/cjs/server/ephemeral/index.cjs +180 -0
  12. package/dist/cjs/server/ephemeral/index.d.cts +102 -0
  13. package/dist/cjs/server/ephemeral/redis.cjs +226 -0
  14. package/dist/cjs/server/ephemeral/redis.d.cts +136 -0
  15. package/dist/cjs/server/index.cjs +238 -87
  16. package/dist/cjs/server/index.d.cts +196 -5
  17. package/dist/cjs/svelte/index.cjs +34 -19
  18. package/dist/cjs/svelte/index.d.cts +18 -3
  19. package/dist/cjs/transport/bun-ws.cjs +9 -9
  20. package/dist/cjs/transport/bun-ws.d.cts +1 -1
  21. package/dist/cjs/transport/polling.cjs +11 -11
  22. package/dist/cjs/transport/polling.d.cts +1 -1
  23. package/dist/cjs/transport/sse.cjs +11 -11
  24. package/dist/cjs/transport/sse.d.cts +1 -1
  25. package/dist/cjs/transport/ws.cjs +11 -11
  26. package/dist/cjs/transport/ws.d.cts +1 -1
  27. package/dist/cjs/vanilla/index.cjs +34 -19
  28. package/dist/cjs/vanilla/index.d.cts +18 -3
  29. package/dist/client/index.d.ts +18 -3
  30. package/dist/client/index.js +7 -7
  31. package/dist/client/storage/indexeddb.d.ts +1 -1
  32. package/dist/client/storage/indexeddb.js +1 -1
  33. package/dist/core/index.d.ts +21 -9
  34. package/dist/core/index.js +20 -20
  35. package/dist/react/index.d.ts +23 -5
  36. package/dist/react/index.js +23 -23
  37. package/dist/server/drizzle.d.ts +1 -1
  38. package/dist/server/drizzle.js +3 -3
  39. package/dist/server/ephemeral/index.d.ts +102 -0
  40. package/dist/server/ephemeral/index.js +9 -0
  41. package/dist/server/ephemeral/redis.d.ts +136 -0
  42. package/dist/server/ephemeral/redis.js +186 -0
  43. package/dist/server/index.d.ts +196 -5
  44. package/dist/server/index.js +184 -163
  45. package/dist/shared/{esm-ytrd3hbq.js → esm-8qbr4y0d.js} +18 -3
  46. package/dist/shared/{esm-wkwx6bd9.js → esm-ck88h30s.js} +10 -10
  47. package/dist/shared/esm-g0marxk7.js +134 -0
  48. package/dist/svelte/index.d.ts +18 -3
  49. package/dist/svelte/index.js +9 -9
  50. package/dist/transport/bun-ws.d.ts +1 -1
  51. package/dist/transport/bun-ws.js +1 -1
  52. package/dist/transport/polling.d.ts +1 -1
  53. package/dist/transport/polling.js +3 -3
  54. package/dist/transport/sse.d.ts +1 -1
  55. package/dist/transport/sse.js +3 -3
  56. package/dist/transport/ws.d.ts +1 -1
  57. package/dist/transport/ws.js +3 -3
  58. package/dist/vanilla/index.d.ts +18 -3
  59. package/dist/vanilla/index.js +9 -9
  60. package/package.json +66 -44
  61. /package/dist/shared/{esm-b7xs9cde.js → esm-k7kedp3y.js} +0 -0
@@ -250,11 +250,15 @@ interface DrizzleTableLike {
250
250
  /**
251
251
  * Value supplied per serverSet field in the schema's object form.
252
252
  * Either a static value or a function that receives the request context.
253
+ *
254
+ * The static arm is enumerated rather than written as `unknown`: a union with
255
+ * `unknown` collapses to `unknown`, which strips the contextual type from the
256
+ * callback arm and makes every `(ctx) => …` an implicit `any` under `strict`.
253
257
  */
254
- type ServerSetSchemaValue = unknown | ((ctx: {
258
+ type ServerSetSchemaValue = ((ctx: {
255
259
  auth: unknown;
256
260
  params: unknown;
257
- }) => unknown);
261
+ }) => unknown) | string | number | bigint | boolean | symbol | null | undefined | Date | readonly unknown[] | Record<string, unknown>;
258
262
  /**
259
263
  * Schema-side `serverSet` declaration. Two shapes:
260
264
  * - `string[]` — keys only; values are supplied at `implement(...)` time.
@@ -319,6 +323,88 @@ type InferServerSetKeys<TDef> = TDef extends {
319
323
  serverSet: infer O extends Readonly<Record<string, unknown>>;
320
324
  } ? keyof O & string : never;
321
325
  /**
326
+ * Ephemeral (presence, cursors, typing) storage and fan-out seam.
327
+ *
328
+ * The default adapter keeps state in the server process, which is correct for a
329
+ * single node and invisible across a fleet: two clients on different instances
330
+ * never see each other. An adapter backed by shared infrastructure (Redis, or a
331
+ * hosted service) fixes both halves — the shared store answers "who is here"
332
+ * for a client that just joined, and the bus carries each event to the peers
333
+ * holding the other sockets.
334
+ */
335
+ type MaybePromise<T> = T | Promise<T>;
336
+ interface EphemeralState {
337
+ clientId: string;
338
+ userId: string;
339
+ key: string;
340
+ data: Record<string, unknown>;
341
+ updatedAt: number;
342
+ ttlMs?: number;
343
+ }
344
+ /** One fan-out target: subscribers of `query`, narrowed to `room` when set. */
345
+ interface EphemeralTarget {
346
+ query: string;
347
+ room: string | null;
348
+ }
349
+ /**
350
+ * An ephemeral event as it crosses the bus between server instances.
351
+ *
352
+ * Recipients are resolved on the origin instance and carried here as `targets`,
353
+ * because the receiving instance holds no session for the sender and so cannot
354
+ * re-derive them. `serverId` lets an instance drop its own echo on buses that
355
+ * deliver published messages back to the publisher.
356
+ */
357
+ interface EphemeralBroadcast {
358
+ serverId: string;
359
+ key: string;
360
+ clientId: string;
361
+ userId: string;
362
+ data: Record<string, unknown>;
363
+ ttlMs?: number;
364
+ targets: EphemeralTarget[];
365
+ }
366
+ /**
367
+ * Backing store and fan-out bus for ephemeral state.
368
+ *
369
+ * `publish`/`subscribe` are optional: an adapter that omits them is a
370
+ * single-process store, and the handler still fans out to its own sockets.
371
+ */
372
+ interface EphemeralAdapter {
373
+ /**
374
+ * Record one entry, keyed by `clientId`.
375
+ *
376
+ * Peer identity is the connection, not the account: two tabs from one login
377
+ * are two cursors, and the client bindings key peers the same way. `userId`
378
+ * rides along for display and authorization, not as the entry's identity.
379
+ *
380
+ * Returns false when the adapter is at capacity, which the handler surfaces
381
+ * as `ephemeral_full` rather than evicting silently.
382
+ */
383
+ set(room: string, key: string, clientId: string, userId: string, data: Record<string, unknown>, ttlMs?: number): MaybePromise<boolean>;
384
+ /** Live entries for one channel, keyed by clientId. */
385
+ get(room: string, key: string): MaybePromise<Record<string, EphemeralState>>;
386
+ /**
387
+ * Live entries for every channel in a room, keyed by channel key then
388
+ * clientId. Backs the snapshot a client receives when it joins.
389
+ */
390
+ getRoom(room: string): MaybePromise<Record<string, Record<string, EphemeralState>>>;
391
+ remove(room: string, key: string, clientId: string): MaybePromise<void>;
392
+ /** Drop everything a disconnecting client published. */
393
+ removeClient(clientId: string): MaybePromise<void>;
394
+ /** Sweep entries past their TTL. Called on a timer by the handler. */
395
+ cleanupExpired(): MaybePromise<void>;
396
+ /** Current entry count, for the capacity gate and `ephemeral_full`. */
397
+ size(): MaybePromise<number>;
398
+ destroy(): MaybePromise<void>;
399
+ /** Hand an event to peer instances. Absent on single-process adapters. */
400
+ publish?(event: EphemeralBroadcast): MaybePromise<void>;
401
+ /**
402
+ * Register the handler's delivery callback for events published by peers.
403
+ * Called once during wiring. Absent on single-process adapters.
404
+ */
405
+ subscribe?(onEvent: (event: EphemeralBroadcast) => void): MaybePromise<void>;
406
+ }
407
+ /**
322
408
  * Adapter contract for `server.tx({ atomic: true })` — pluggable so non-drizzle
323
409
  * data layers (kysely, prisma, raw SQL) can wrap their own BEGIN/COMMIT/ROLLBACK
324
410
  * without reflectdb hard-coding a drizzle dependency.
@@ -456,6 +542,16 @@ declare class ResultCache {
456
542
  private cache;
457
543
  /**
458
544
  * Update the cached result set and return the diff.
545
+ *
546
+ * Rows are snapshotted, not referenced. A query that hands back live objects
547
+ * — an in-memory store, a game loop mutating rows in place, an ORM returning
548
+ * tracked entities — would otherwise have the cache holding the very objects
549
+ * the next write mutates, so every later diff compares a row against itself,
550
+ * finds nothing changed, and the client silently stops receiving updates.
551
+ *
552
+ * The copy is shallow: mutating a nested object inside a row in place is
553
+ * still invisible to change detection.
554
+ *
459
555
  * @param idField - The field name used as the row identifier (default: "id")
460
556
  */
461
557
  set(clientId: string, queryName: string, rows: Record<string, unknown>[], idField?: string): DiffResult;
@@ -467,6 +563,21 @@ declare class ResultCache {
467
563
  * already has rows it never received.
468
564
  */
469
565
  diffOnly(clientId: string, queryName: string, rows: Record<string, unknown>[], idField?: string): DiffResult;
566
+ /**
567
+ * Forget one row of one client's cached result.
568
+ *
569
+ * The cache mirrors what a client is believed to hold, and a writer is
570
+ * excluded from the broadcast of its own write because it already applied
571
+ * that write optimistically. For a delete that means the row is gone on the
572
+ * client while the cache still holds it — and if the same rowId is later
573
+ * re-created, the next diff reports it as an *update* against the dead row
574
+ * and sends only the fields that happen to differ. The client, whose local
575
+ * row is just its own optimistic payload, would silently never receive the
576
+ * unchanged server-owned columns. Dropping the row here keeps the mirror
577
+ * honest, so a re-created rowId diffs as a fresh insert carrying every
578
+ * column.
579
+ */
580
+ evictRow(clientId: string, queryName: string, rowId: string): void;
470
581
  clear(clientId: string, queryName: string): void;
471
582
  clearClient(clientId: string): void;
472
583
  private getOrCreateClientCache;
@@ -662,6 +773,8 @@ declare class MessageHandler<TAuth extends AuthContext = AuthContext> {
662
773
  private broadcast;
663
774
  private ops;
664
775
  private ephemeralManager;
776
+ /** Bus subscription is wired once, on the first adapter that offers one. */
777
+ private ephemeralBusReady;
665
778
  private ephemeralCleanupTimer;
666
779
  private eagerBuffer;
667
780
  private replay;
@@ -701,6 +814,13 @@ declare class MessageHandler<TAuth extends AuthContext = AuthContext> {
701
814
  setEphemeralRateLimit(perSecond: number): void;
702
815
  setMaxBatchSize(size: number): void;
703
816
  setMaxEphemeralEntries(max: number): void;
817
+ /**
818
+ * Swap the ephemeral store. An adapter that implements `subscribe` also
819
+ * makes this instance a bus participant, so presence spans the fleet.
820
+ */
821
+ setEphemeralAdapter(adapter: EphemeralAdapter): void;
822
+ /** Drop everything a client published, whatever the adapter costs. */
823
+ private forgetEphemeralClient;
704
824
  setMinSchemaVersion(version: number): void;
705
825
  setMaxConnectionsPerUser(max: number | null): void;
706
826
  private enforceConnectionCap;
@@ -761,6 +881,23 @@ declare class MessageHandler<TAuth extends AuthContext = AuthContext> {
761
881
  */
762
882
  private allowEphemeral;
763
883
  private handleEphemeral;
884
+ /**
885
+ * Deliver one ephemeral event to this instance's sockets.
886
+ *
887
+ * `exclude` is the sender when the event originated here, and undefined
888
+ * when it arrived over the bus — the sender's socket lives elsewhere.
889
+ */
890
+ private fanOutEphemeral;
891
+ /** An event published by a peer instance. State is already in the shared store. */
892
+ private deliverRemoteEphemeral;
893
+ /**
894
+ * Replay a room's live presence to a client that just subscribed.
895
+ *
896
+ * Without this a joiner sees nobody until each peer happens to move again —
897
+ * the stored state existed but was never served. Replayed as ordinary
898
+ * `ephemeral` events, so clients need no new message type to benefit.
899
+ */
900
+ private sendPresenceSnapshot;
764
901
  /** Public reserve-or-replay gate for REST idempotency. Returns true when fresh. */
765
902
  reserveOpId(opId: string): Promise<boolean>;
766
903
  runCompaction(minOpAge: number): Promise<number>;
@@ -915,6 +1052,18 @@ interface ServerConfig<TDb = unknown> {
915
1052
  * this is absent. See `TxAtomicAdapter`.
916
1053
  */
917
1054
  txAtomic?: TxAtomicAdapter;
1055
+ /**
1056
+ * Ephemeral (presence, cursors, typing) storage and fan-out.
1057
+ *
1058
+ * The default is in-process: correct on one node, invisible across a fleet,
1059
+ * since each instance holds its own map and its own sockets. Supply an
1060
+ * adapter backed by shared infrastructure to make presence span instances.
1061
+ */
1062
+ ephemeral?: {
1063
+ adapter?: EphemeralAdapter;
1064
+ /** Entry ceiling for the default in-process store. Default: 10_000. */
1065
+ maxEntries?: number;
1066
+ };
918
1067
  }
919
1068
  interface QueryContext<TAuth extends AuthContext = AuthContext> {
920
1069
  auth: TAuth;
@@ -1472,6 +1621,11 @@ interface BunStatement<Row> {
1472
1621
  run(...params: SqlValue[]): {
1473
1622
  changes: number;
1474
1623
  };
1624
+ /**
1625
+ * Releases the statement's sqlite handle. Optional so a hand-rolled stand-in
1626
+ * need not implement it; `bun:sqlite` always does.
1627
+ */
1628
+ finalize?(): void;
1475
1629
  }
1476
1630
  interface BunDatabase {
1477
1631
  run(sql: string, ...params: SqlValue[][]): {
@@ -1564,6 +1718,18 @@ declare class BroadcastEngine<TAuth extends AuthContext = AuthContext> {
1564
1718
  private withLock;
1565
1719
  private executeWithTimeout;
1566
1720
  /**
1721
+ * Forget a row the writer itself removed, in every query that depends on
1722
+ * `tableName`.
1723
+ *
1724
+ * The writer is skipped by `broadcastChanges` because it applied its own op
1725
+ * optimistically, which leaves its cached result claiming a row the client
1726
+ * has already dropped. Re-creating that same rowId would then diff as an
1727
+ * update against the dead row and send only the columns that differ, so the
1728
+ * writer would never receive the server-owned columns that happen to match
1729
+ * the row it deleted.
1730
+ */
1731
+ forgetWriterRow(tableName: string, clientId: string, rowId: string): void;
1732
+ /**
1567
1733
  * After a write to `tableName`, re-run every dependent query and send each
1568
1734
  * subscriber the rows that changed for them.
1569
1735
  *
@@ -1647,6 +1813,13 @@ interface OpProcessorDeps<TAuth extends AuthContext> {
1647
1813
  receiveClientHlc(hlc: string): void;
1648
1814
  send(clientId: string, message: ServerMessage): Promise<void>;
1649
1815
  broadcastChanges(tableName: string, excludeClientId: string): Promise<void>;
1816
+ /**
1817
+ * Drop a row the writer deleted from the writer's own cached result set.
1818
+ * The writer is excluded from the broadcast of its own op, so without this
1819
+ * the cache keeps a row the client no longer holds — and a later re-insert
1820
+ * of the same rowId diffs as a partial update against that dead row.
1821
+ */
1822
+ forgetWriterRow(tableName: string, clientId: string, rowId: string): void;
1650
1823
  }
1651
1824
  /**
1652
1825
  * Applies client ops: replay reservation, enforcement, conflict resolution,
@@ -1795,6 +1968,16 @@ interface TypedServerConfig<
1795
1968
  maxBroadcastConcurrency?: number;
1796
1969
  /** Adapter that wraps `server.tx({ atomic: true })` writes in a transaction. */
1797
1970
  txAtomic?: TxAtomicAdapter;
1971
+ /**
1972
+ * Ephemeral (presence, cursors, typing) storage and fan-out. Defaults to an
1973
+ * in-process store, which is invisible across a fleet — supply an adapter
1974
+ * backed by shared infrastructure to make presence span instances.
1975
+ */
1976
+ ephemeral?: {
1977
+ adapter?: EphemeralAdapter;
1978
+ /** Entry ceiling for the default in-process store. Default: 10_000. */
1979
+ maxEntries?: number;
1980
+ };
1798
1981
  }
1799
1982
  type ImplementParams<
1800
1983
  TQueries extends SyncQueryMap,
@@ -1818,14 +2001,22 @@ type IsServerSetArrayForm<
1818
2001
  serverSet: readonly string[];
1819
2002
  } ? true : false;
1820
2003
  /** Value: static or a function receiving auth/params context */
2004
+ /**
2005
+ * A serverSet entry: a static value, or a function of the request context.
2006
+ *
2007
+ * The static arm is spelled out rather than written as `unknown`, because
2008
+ * `unknown | ((ctx) => …)` collapses to `unknown` and the callback's parameter
2009
+ * then has no contextual type — every `(ctx) => …` would be an implicit `any`
2010
+ * under `strict`.
2011
+ */
1821
2012
  type ServerSetValue<
1822
2013
  TAuth extends AuthContext,
1823
2014
  TQueries extends SyncQueryMap,
1824
2015
  K extends keyof TQueries
1825
- > = unknown | ((ctx: {
2016
+ > = ((ctx: {
1826
2017
  auth: TAuth;
1827
2018
  params: ImplementParams<TQueries, K>;
1828
- }) => unknown);
2019
+ }) => unknown) | string | number | bigint | boolean | symbol | null | undefined | Date | readonly unknown[] | Record<string, unknown>;
1829
2020
  /**
1830
2021
  * `serverSet` shape on `implement(...)`:
1831
2022
  * - schema declares no `serverSet` → field disallowed.
@@ -1985,4 +2176,4 @@ declare function createSyncServer<
1985
2176
  TDb = unknown,
1986
2177
  TAuth extends AuthContext = AuthContext
1987
2178
  >(config: TypedServerConfig<TQueries, TDb>): TypedSyncServer<TQueries, TAuth, TDb>;
1988
- export { stableStringify, resolveRoomKey, resolveConflict, processOp, enforceServerSet, enforceReadonly, enforceClockDrift, enforceBatchSize, drizzleTxAtomic, drizzleTable, defineTable, createSyncServer, createSqliteStorage, createServer, createRateLimiter, createPostgresStorage, TypedSyncServer, TypedServerConfig, TxProxy, TxOptions, TxFn, TxAtomicAdapter, TableAdapter, SyncServer, SyncEvent, StorageAdapter, SqliteStorageConfig, SessionManager, ServerConfig, ServerClock, ScopeFilter, SERVER_HLC_META_KEY, RoomResolution, RoomCallback, ResultCache, RestConfig, ResolvedOp, RateLimiter, QuerySubscription, QueryRegistration, QueryOptions, QueryContext, QueryCallback, PostgresStorageConfig, PostgresClient, PipelineResult, PipelineContext, OpResult, OpProcessorDeps, OpProcessor, OpLogEntry, MutationError, MutationContext, MutateResult, MessageHandler, ImplementOptions, HandlerConfig, ExistingRow, EnforcementResult, EnforcementContext, DrizzleTableOpts, DiffResult, DefineTableOpts, ConflictResult, ConflictInput, ClientSession, BroadcastEngineDeps, BroadcastEngine, BatchContext, AuthorizeAction, AuthCallback };
2179
+ export { AuthCallback, AuthorizeAction, BatchContext, BroadcastEngine, BroadcastEngineDeps, ClientSession, ConflictInput, ConflictResult, DefineTableOpts, DiffResult, DrizzleTableOpts, EnforcementContext, EnforcementResult, ExistingRow, HandlerConfig, ImplementOptions, MessageHandler, MutateResult, MutationContext, MutationError, OpLogEntry, OpProcessor, OpProcessorDeps, OpResult, PipelineContext, PipelineResult, PostgresClient, PostgresStorageConfig, QueryCallback, QueryContext, QueryOptions, QueryRegistration, QuerySubscription, RateLimiter, ResolvedOp, RestConfig, ResultCache, RoomCallback, RoomResolution, SERVER_HLC_META_KEY, ScopeFilter, ServerClock, ServerConfig, SessionManager, SqliteStorageConfig, StorageAdapter, SyncEvent, SyncServer, TableAdapter, TxAtomicAdapter, TxFn, TxOptions, TxProxy, TypedServerConfig, TypedSyncServer, createPostgresStorage, createRateLimiter, createServer, createSqliteStorage, createSyncServer, defineTable, drizzleTable, drizzleTxAtomic, enforceBatchSize, enforceClockDrift, enforceReadonly, enforceServerSet, processOp, resolveConflict, resolveRoomKey, stableStringify };
@@ -41,16 +41,16 @@ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
41
41
  // src/core/types.ts
42
42
  var exports_types = {};
43
43
  __export(exports_types, {
44
- reasonFromError: () => reasonFromError,
45
- isErrorReason: () => isErrorReason,
46
- TransportSendError: () => TransportSendError,
47
- TOMBSTONE_RETENTION_MS: () => TOMBSTONE_RETENTION_MS,
48
- SUPPORTED_PROTOCOL_VERSIONS: () => SUPPORTED_PROTOCOL_VERSIONS,
49
- SERVER_TOMBSTONE_RETENTION_MS: () => SERVER_TOMBSTONE_RETENTION_MS,
50
- PROTOCOL_VERSION: () => PROTOCOL_VERSION,
51
- MutationError: () => MutationError,
44
+ MAX_BATCH_SIZE: () => MAX_BATCH_SIZE,
52
45
  MAX_CLOCK_DRIFT_MS: () => MAX_CLOCK_DRIFT_MS,
53
- MAX_BATCH_SIZE: () => MAX_BATCH_SIZE
46
+ MutationError: () => MutationError,
47
+ PROTOCOL_VERSION: () => PROTOCOL_VERSION,
48
+ SERVER_TOMBSTONE_RETENTION_MS: () => SERVER_TOMBSTONE_RETENTION_MS,
49
+ SUPPORTED_PROTOCOL_VERSIONS: () => SUPPORTED_PROTOCOL_VERSIONS,
50
+ TOMBSTONE_RETENTION_MS: () => TOMBSTONE_RETENTION_MS,
51
+ TransportSendError: () => TransportSendError,
52
+ isErrorReason: () => isErrorReason,
53
+ reasonFromError: () => reasonFromError
54
54
  });
55
55
  function isErrorReason(value) {
56
56
  return typeof value === "string" && VALID_ERROR_REASONS.has(value);
@@ -107,9 +107,9 @@ var init_types = __esm(() => {
107
107
  // src/svelte/index.ts
108
108
  var exports_svelte = {};
109
109
  __export(exports_svelte, {
110
- createSyncSvelte: () => createSyncSvelte,
110
+ createBrowserWsTransport: () => createBrowserWsTransport,
111
111
  createSyncStore: () => createSyncStore,
112
- createBrowserWsTransport: () => createBrowserWsTransport
112
+ createSyncSvelte: () => createSyncSvelte
113
113
  });
114
114
  module.exports = __toCommonJS(exports_svelte);
115
115
 
@@ -415,7 +415,7 @@ class ClientStore {
415
415
  if (existing?.serverHlc && compareHlc(hlc, existing.serverHlc) <= 0) {
416
416
  return;
417
417
  }
418
- this.setRow(table, rowId, payload ?? {}, colClocks ?? { _row: hlc }, hlc);
418
+ this.setRow(table, rowId, this.withPk(table, rowId, payload ?? {}), colClocks ?? { _row: hlc }, hlc);
419
419
  return;
420
420
  }
421
421
  const incomingClocks = colClocks ?? {};
@@ -436,7 +436,13 @@ class ClientStore {
436
436
  }
437
437
  const newRowHlc = existing.serverHlc && compareHlc(existing.serverHlc, hlc) >= 0 ? existing.serverHlc : hlc;
438
438
  mergedClocks._row = newRowHlc;
439
- this.setRow(table, rowId, mergedData, mergedClocks, newRowHlc);
439
+ this.setRow(table, rowId, this.withPk(table, rowId, mergedData), mergedClocks, newRowHlc);
440
+ }
441
+ withPk(table, rowId, data) {
442
+ const pk = this.tableMeta.get(table)?.pk ?? "id";
443
+ if (data[pk] !== undefined)
444
+ return data;
445
+ return { ...data, [pk]: rowId };
440
446
  }
441
447
  revertOp(opId, serverRow) {
442
448
  const pending = this.pendingOps.find((p) => p.op.id === opId);
@@ -481,10 +487,19 @@ class ClientStore {
481
487
  delete payload[field];
482
488
  }
483
489
  }
490
+ if (op.op === "insert" && payload) {
491
+ payload = this.withPk(op.table, op.rowId, payload);
492
+ }
484
493
  if (op.op === "delete") {
485
494
  this.setRow(op.table, op.rowId, null, { _row: op.hlc }, op.hlc);
486
495
  } else if (op.op === "insert") {
487
- this.setRow(op.table, op.rowId, payload ?? {}, { _row: op.hlc }, op.hlc);
496
+ const existing = this.getRow(op.table, op.rowId);
497
+ if (existing?.data) {
498
+ const merged = { ...existing.data, ...payload };
499
+ this.setRow(op.table, op.rowId, merged, existing.colClocks, existing.serverHlc);
500
+ } else {
501
+ this.setRow(op.table, op.rowId, payload ?? {}, { _row: op.hlc }, op.hlc);
502
+ }
488
503
  } else {
489
504
  const existing = this.getRow(op.table, op.rowId);
490
505
  if (existing?.data) {
@@ -1323,21 +1338,21 @@ function createSyncStore(config) {
1323
1338
  }
1324
1339
  }
1325
1340
  const unsub = client.subscribeEphemeral(cfg.key, (event) => {
1326
- current = { ...current, [event.userId]: event.data };
1341
+ current = { ...current, [event.clientId]: event.data };
1327
1342
  notifySubscribers();
1328
1343
  if (cfg.ttlMs) {
1329
- const existing = timers.get(event.userId);
1344
+ const existing = timers.get(event.clientId);
1330
1345
  if (existing) {
1331
1346
  clearTimeout(existing);
1332
1347
  }
1333
1348
  const timer = setTimeout(() => {
1334
1349
  const next = { ...current };
1335
- delete next[event.userId];
1350
+ delete next[event.clientId];
1336
1351
  current = next;
1337
- timers.delete(event.userId);
1352
+ timers.delete(event.clientId);
1338
1353
  notifySubscribers();
1339
1354
  }, cfg.ttlMs);
1340
- timers.set(event.userId, timer);
1355
+ timers.set(event.clientId, timer);
1341
1356
  }
1342
1357
  });
1343
1358
  const events = {
@@ -267,6 +267,17 @@ declare class ClientStore {
267
267
  clearTable(table: string): void;
268
268
  applySnapshot(table: string, rows: Record<string, unknown>[], colClocks: Record<string, Record<string, string>>, append?: boolean, pk?: string): void;
269
269
  applyDelta(table: string, op: string, rowId: string, payload: Record<string, unknown> | null, hlc: string, colClocks?: Record<string, string>): void;
270
+ /**
271
+ * Guarantee a materialized row carries its own primary key.
272
+ *
273
+ * A delta's payload is not always a whole row: eager broadcasts forward the
274
+ * writer's payload verbatim, and the typed API omits the pk from what a
275
+ * client may write — so an eagerly-broadcast insert describes a row with no
276
+ * id in it. Rows are addressed by `rowId` everywhere in the protocol, so the
277
+ * key is never in doubt; only the materialized object was missing it, and
278
+ * `rows.map(r => r.id)` came back undefined.
279
+ */
280
+ private withPk;
270
281
  revertOp(opId: string, serverRow: Record<string, unknown> | null | undefined): void;
271
282
  setTableMeta(meta: Record<string, {
272
283
  serverSet: string[];
@@ -496,11 +507,15 @@ interface DrizzleTableLike {
496
507
  /**
497
508
  * Value supplied per serverSet field in the schema's object form.
498
509
  * Either a static value or a function that receives the request context.
510
+ *
511
+ * The static arm is enumerated rather than written as `unknown`: a union with
512
+ * `unknown` collapses to `unknown`, which strips the contextual type from the
513
+ * callback arm and makes every `(ctx) => …` an implicit `any` under `strict`.
499
514
  */
500
- type ServerSetSchemaValue = unknown | ((ctx: {
515
+ type ServerSetSchemaValue = ((ctx: {
501
516
  auth: unknown;
502
517
  params: unknown;
503
- }) => unknown);
518
+ }) => unknown) | string | number | bigint | boolean | symbol | null | undefined | Date | readonly unknown[] | Record<string, unknown>;
504
519
  /**
505
520
  * Schema-side `serverSet` declaration. Two shapes:
506
521
  * - `string[]` — keys only; values are supplied at `implement(...)` time.
@@ -642,4 +657,4 @@ interface SyncSvelteHooks<TQueries extends SyncQueryMap> {
642
657
  createStore: (config: SyncStoreConfig) => TypedSyncStore<TQueries>;
643
658
  }
644
659
  declare function createSyncSvelte<TQueries extends SyncQueryMap>(): SyncSvelteHooks<TQueries>;
645
- export { createSyncSvelte, createSyncStore, createBrowserWsTransport, TypedSyncStore, SyncSvelteHooks, SyncStoreConfig, SyncStore, Readable };
660
+ export { Readable, SyncStore, SyncStoreConfig, SyncSvelteHooks, TypedSyncStore, createBrowserWsTransport, createSyncStore, createSyncSvelte };
@@ -41,16 +41,16 @@ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
41
41
  // src/core/types.ts
42
42
  var exports_types = {};
43
43
  __export(exports_types, {
44
- reasonFromError: () => reasonFromError,
45
- isErrorReason: () => isErrorReason,
46
- TransportSendError: () => TransportSendError,
47
- TOMBSTONE_RETENTION_MS: () => TOMBSTONE_RETENTION_MS,
48
- SUPPORTED_PROTOCOL_VERSIONS: () => SUPPORTED_PROTOCOL_VERSIONS,
49
- SERVER_TOMBSTONE_RETENTION_MS: () => SERVER_TOMBSTONE_RETENTION_MS,
50
- PROTOCOL_VERSION: () => PROTOCOL_VERSION,
51
- MutationError: () => MutationError,
44
+ MAX_BATCH_SIZE: () => MAX_BATCH_SIZE,
52
45
  MAX_CLOCK_DRIFT_MS: () => MAX_CLOCK_DRIFT_MS,
53
- MAX_BATCH_SIZE: () => MAX_BATCH_SIZE
46
+ MutationError: () => MutationError,
47
+ PROTOCOL_VERSION: () => PROTOCOL_VERSION,
48
+ SERVER_TOMBSTONE_RETENTION_MS: () => SERVER_TOMBSTONE_RETENTION_MS,
49
+ SUPPORTED_PROTOCOL_VERSIONS: () => SUPPORTED_PROTOCOL_VERSIONS,
50
+ TOMBSTONE_RETENTION_MS: () => TOMBSTONE_RETENTION_MS,
51
+ TransportSendError: () => TransportSendError,
52
+ isErrorReason: () => isErrorReason,
53
+ reasonFromError: () => reasonFromError
54
54
  });
55
55
  function isErrorReason(value) {
56
56
  return typeof value === "string" && VALID_ERROR_REASONS.has(value);
@@ -212,4 +212,4 @@ declare function createBunWsServerTransport(cfg?: BunWsServerConfig): {
212
212
  pong?(ws: BunServerWebSocket<BunWsTransportData>): void;
213
213
  };
214
214
  };
215
- export { createBunWsServerTransport, BunWsTransportData, BunWsServerConfig };
215
+ export { BunWsServerConfig, BunWsTransportData, createBunWsServerTransport };
@@ -41,16 +41,16 @@ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
41
41
  // src/core/types.ts
42
42
  var exports_types = {};
43
43
  __export(exports_types, {
44
- reasonFromError: () => reasonFromError,
45
- isErrorReason: () => isErrorReason,
46
- TransportSendError: () => TransportSendError,
47
- TOMBSTONE_RETENTION_MS: () => TOMBSTONE_RETENTION_MS,
48
- SUPPORTED_PROTOCOL_VERSIONS: () => SUPPORTED_PROTOCOL_VERSIONS,
49
- SERVER_TOMBSTONE_RETENTION_MS: () => SERVER_TOMBSTONE_RETENTION_MS,
50
- PROTOCOL_VERSION: () => PROTOCOL_VERSION,
51
- MutationError: () => MutationError,
44
+ MAX_BATCH_SIZE: () => MAX_BATCH_SIZE,
52
45
  MAX_CLOCK_DRIFT_MS: () => MAX_CLOCK_DRIFT_MS,
53
- MAX_BATCH_SIZE: () => MAX_BATCH_SIZE
46
+ MutationError: () => MutationError,
47
+ PROTOCOL_VERSION: () => PROTOCOL_VERSION,
48
+ SERVER_TOMBSTONE_RETENTION_MS: () => SERVER_TOMBSTONE_RETENTION_MS,
49
+ SUPPORTED_PROTOCOL_VERSIONS: () => SUPPORTED_PROTOCOL_VERSIONS,
50
+ TOMBSTONE_RETENTION_MS: () => TOMBSTONE_RETENTION_MS,
51
+ TransportSendError: () => TransportSendError,
52
+ isErrorReason: () => isErrorReason,
53
+ reasonFromError: () => reasonFromError
54
54
  });
55
55
  function isErrorReason(value) {
56
56
  return typeof value === "string" && VALID_ERROR_REASONS.has(value);
@@ -108,9 +108,9 @@ var init_types = __esm(() => {
108
108
  init_types();
109
109
  var exports_polling = {};
110
110
  __export(exports_polling, {
111
- pollingBodyTooLarge: () => pollingBodyTooLarge,
111
+ createPollingClientTransport: () => createPollingClientTransport,
112
112
  createPollingServerTransport: () => createPollingServerTransport,
113
- createPollingClientTransport: () => createPollingClientTransport
113
+ pollingBodyTooLarge: () => pollingBodyTooLarge
114
114
  });
115
115
  module.exports = __toCommonJS(exports_polling);
116
116
  function pollingBodyTooLarge(body, limit = 1e6) {
@@ -207,4 +207,4 @@ interface PollingClientConfig {
207
207
  headers?: Record<string, string>;
208
208
  }
209
209
  declare function createPollingClientTransport(config: PollingClientConfig): ClientTransport;
210
- export { pollingBodyTooLarge, createPollingServerTransport, createPollingClientTransport, PollingServerConfig, PollingClientConfig };
210
+ export { PollingClientConfig, PollingServerConfig, createPollingClientTransport, createPollingServerTransport, pollingBodyTooLarge };
@@ -41,16 +41,16 @@ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
41
41
  // src/core/types.ts
42
42
  var exports_types = {};
43
43
  __export(exports_types, {
44
- reasonFromError: () => reasonFromError,
45
- isErrorReason: () => isErrorReason,
46
- TransportSendError: () => TransportSendError,
47
- TOMBSTONE_RETENTION_MS: () => TOMBSTONE_RETENTION_MS,
48
- SUPPORTED_PROTOCOL_VERSIONS: () => SUPPORTED_PROTOCOL_VERSIONS,
49
- SERVER_TOMBSTONE_RETENTION_MS: () => SERVER_TOMBSTONE_RETENTION_MS,
50
- PROTOCOL_VERSION: () => PROTOCOL_VERSION,
51
- MutationError: () => MutationError,
44
+ MAX_BATCH_SIZE: () => MAX_BATCH_SIZE,
52
45
  MAX_CLOCK_DRIFT_MS: () => MAX_CLOCK_DRIFT_MS,
53
- MAX_BATCH_SIZE: () => MAX_BATCH_SIZE
46
+ MutationError: () => MutationError,
47
+ PROTOCOL_VERSION: () => PROTOCOL_VERSION,
48
+ SERVER_TOMBSTONE_RETENTION_MS: () => SERVER_TOMBSTONE_RETENTION_MS,
49
+ SUPPORTED_PROTOCOL_VERSIONS: () => SUPPORTED_PROTOCOL_VERSIONS,
50
+ TOMBSTONE_RETENTION_MS: () => TOMBSTONE_RETENTION_MS,
51
+ TransportSendError: () => TransportSendError,
52
+ isErrorReason: () => isErrorReason,
53
+ reasonFromError: () => reasonFromError
54
54
  });
55
55
  function isErrorReason(value) {
56
56
  return typeof value === "string" && VALID_ERROR_REASONS.has(value);
@@ -108,8 +108,8 @@ var init_types = __esm(() => {
108
108
  init_types();
109
109
  var exports_sse = {};
110
110
  __export(exports_sse, {
111
- createSseServerTransport: () => createSseServerTransport,
112
- createSseClientTransport: () => createSseClientTransport
111
+ createSseClientTransport: () => createSseClientTransport,
112
+ createSseServerTransport: () => createSseServerTransport
113
113
  });
114
114
  module.exports = __toCommonJS(exports_sse);
115
115
  function createSseServerTransport(cfg = {}) {
@@ -202,4 +202,4 @@ interface SseClientConfig {
202
202
  headers?: Record<string, string>;
203
203
  }
204
204
  declare function createSseClientTransport(config: SseClientConfig): ClientTransport;
205
- export { createSseServerTransport, createSseClientTransport, SseServerConfig, SseClientConfig };
205
+ export { SseClientConfig, SseServerConfig, createSseClientTransport, createSseServerTransport };
@@ -41,16 +41,16 @@ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
41
41
  // src/core/types.ts
42
42
  var exports_types = {};
43
43
  __export(exports_types, {
44
- reasonFromError: () => reasonFromError,
45
- isErrorReason: () => isErrorReason,
46
- TransportSendError: () => TransportSendError,
47
- TOMBSTONE_RETENTION_MS: () => TOMBSTONE_RETENTION_MS,
48
- SUPPORTED_PROTOCOL_VERSIONS: () => SUPPORTED_PROTOCOL_VERSIONS,
49
- SERVER_TOMBSTONE_RETENTION_MS: () => SERVER_TOMBSTONE_RETENTION_MS,
50
- PROTOCOL_VERSION: () => PROTOCOL_VERSION,
51
- MutationError: () => MutationError,
44
+ MAX_BATCH_SIZE: () => MAX_BATCH_SIZE,
52
45
  MAX_CLOCK_DRIFT_MS: () => MAX_CLOCK_DRIFT_MS,
53
- MAX_BATCH_SIZE: () => MAX_BATCH_SIZE
46
+ MutationError: () => MutationError,
47
+ PROTOCOL_VERSION: () => PROTOCOL_VERSION,
48
+ SERVER_TOMBSTONE_RETENTION_MS: () => SERVER_TOMBSTONE_RETENTION_MS,
49
+ SUPPORTED_PROTOCOL_VERSIONS: () => SUPPORTED_PROTOCOL_VERSIONS,
50
+ TOMBSTONE_RETENTION_MS: () => TOMBSTONE_RETENTION_MS,
51
+ TransportSendError: () => TransportSendError,
52
+ isErrorReason: () => isErrorReason,
53
+ reasonFromError: () => reasonFromError
54
54
  });
55
55
  function isErrorReason(value) {
56
56
  return typeof value === "string" && VALID_ERROR_REASONS.has(value);
@@ -108,9 +108,9 @@ var init_types = __esm(() => {
108
108
  init_types();
109
109
  var exports_ws = {};
110
110
  __export(exports_ws, {
111
- isOriginAllowed: () => isOriginAllowed,
111
+ createWsClientTransport: () => createWsClientTransport,
112
112
  createWsServerTransport: () => createWsServerTransport,
113
- createWsClientTransport: () => createWsClientTransport
113
+ isOriginAllowed: () => isOriginAllowed
114
114
  });
115
115
  module.exports = __toCommonJS(exports_ws);
116
116
  function trySend(clientId, ws, data, maxBufferedBytes) {
@@ -233,4 +233,4 @@ interface WebSocketLike {
233
233
  /** Bytes queued but not yet flushed. Used for outbound backpressure. */
234
234
  bufferedAmount?: number;
235
235
  }
236
- export { isOriginAllowed, createWsServerTransport, createWsClientTransport, WsServerTransportConfig, WsServerConfig, WsClientConfig, WebSocketLike };
236
+ export { WebSocketLike, WsClientConfig, WsServerConfig, WsServerTransportConfig, createWsClientTransport, createWsServerTransport, isOriginAllowed };