@fieldnotes/sync-redis 0.3.0 → 0.3.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.
package/README.md CHANGED
@@ -81,6 +81,10 @@ publishes its live ops to a single channel and every other instance forwards the
81
81
  Like the backend, it has **no Redis dependency of its own** — you inject two connections via the
82
82
  `RedisPublisher` / `RedisSubscriber` seams.
83
83
 
84
+ `publish()` resolves only after Redis acknowledges the command. If Redis rejects it, the fanout
85
+ invokes the optional `onError` observer and rethrows so `SyncHub` can surface the failed durable
86
+ mutation publication. Do not discard the returned promise when publishing directly.
87
+
84
88
  > **The subscriber MUST be a dedicated connection.** Redis forbids any other command on a connection that is
85
89
  > in subscribe mode, so publish and subscribe cannot share one connection — that is why `RedisHubFanout`
86
90
  > takes two.
package/dist/index.cjs CHANGED
@@ -84,8 +84,16 @@ var RedisHubFanout = class {
84
84
  this.channel = options.channel ?? "fieldnotes:fanout";
85
85
  this.onError = options.onError ?? (() => void 0);
86
86
  }
87
- publish(payload) {
88
- Promise.resolve(this.publisher.publish(this.channel, payload)).catch(this.onError);
87
+ async publish(payload) {
88
+ try {
89
+ await this.publisher.publish(this.channel, payload);
90
+ } catch (error) {
91
+ try {
92
+ this.onError(error);
93
+ } catch {
94
+ }
95
+ throw error;
96
+ }
89
97
  }
90
98
  subscribe(handler) {
91
99
  this.handlers.add(handler);
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/redis-hub-backend.ts","../src/redis-hub-fanout.ts"],"sourcesContent":["export { RedisHubBackend } from './redis-hub-backend';\r\nexport type { RedisHubBackendOptions } from './redis-hub-backend';\r\nexport type { RedisHashClient } from './redis-hash-client';\r\nexport { RedisHubFanout } from './redis-hub-fanout';\r\nexport type { RedisHubFanoutOptions } from './redis-hub-fanout';\r\nexport type { RedisPublisher, RedisSubscriber } from './redis-fanout-client';\r\n","import { isValidElement, type SyncOp } from '@fieldnotes/sync';\nimport type { CanvasElement } from '@fieldnotes/core';\nimport type { HubBackend } from '@fieldnotes/sync-server';\nimport type { RedisHashClient } from './redis-hash-client';\n\nexport interface RedisHubBackendOptions {\n keyPrefix?: string; // default 'fieldnotes:room:'\n}\n\nexport class RedisHubBackend implements HubBackend {\n private readonly client: RedisHashClient;\n private readonly keyPrefix: string;\n\n constructor(client: RedisHashClient, options: RedisHubBackendOptions = {}) {\n this.client = client;\n this.keyPrefix = options.keyPrefix ?? 'fieldnotes:room:';\n }\n\n private key(room: string): string {\n return `${this.keyPrefix}${room}`;\n }\n\n async snapshot(room: string): Promise<CanvasElement[]> {\n const map = await this.client.hGetAll(this.key(room));\n const out: CanvasElement[] = [];\n for (const value of Object.values(map)) {\n let parsed: unknown;\n try {\n parsed = JSON.parse(value);\n } catch {\n continue; // skip a corrupt stored value rather than throwing the whole snapshot\n }\n if (isValidElement(parsed)) out.push(parsed);\n }\n return out;\n }\n\n async get(room: string, id: string): Promise<CanvasElement | undefined> {\n const value = await this.client.hGet(this.key(room), id);\n if (value == null) return undefined;\n try {\n const parsed: unknown = JSON.parse(value);\n return isValidElement(parsed) ? parsed : undefined;\n } catch {\n return undefined;\n }\n }\n\n async apply(room: string, op: SyncOp): Promise<void> {\n const key = this.key(room);\n if (op.kind === 'upsert')\n await this.client.hSet(key, op.element.id, JSON.stringify(op.element));\n else if (op.kind === 'remove') await this.client.hDel(key, op.id);\n else if (op.kind === 'clear') await this.client.del(key);\n // request-snapshot/snapshot never reach apply (the hub only applies data ops)\n }\n}\n","import type { HubFanout } from '@fieldnotes/sync-server';\r\nimport type { RedisPublisher, RedisSubscriber } from './redis-fanout-client';\r\n\r\nexport interface RedisHubFanoutOptions {\r\n channel?: string;\r\n onError?: (err: unknown) => void;\r\n}\r\n\r\nexport class RedisHubFanout implements HubFanout {\r\n private readonly publisher: RedisPublisher;\r\n private readonly subscriber: RedisSubscriber;\r\n private readonly channel: string;\r\n private readonly onError: (err: unknown) => void;\r\n private readonly handlers = new Set<(payload: string) => void>();\r\n private subscribed = false;\r\n\r\n constructor(\r\n publisher: RedisPublisher,\r\n subscriber: RedisSubscriber,\r\n options: RedisHubFanoutOptions = {},\r\n ) {\r\n this.publisher = publisher;\r\n this.subscriber = subscriber;\r\n this.channel = options.channel ?? 'fieldnotes:fanout';\r\n this.onError = options.onError ?? (() => undefined);\r\n }\r\n\r\n publish(payload: string): void {\r\n Promise.resolve(this.publisher.publish(this.channel, payload)).catch(this.onError);\r\n }\r\n\r\n subscribe(handler: (payload: string) => void): () => void {\r\n this.handlers.add(handler);\r\n if (!this.subscribed) {\r\n this.subscribed = true;\r\n Promise.resolve(\r\n this.subscriber.subscribe(this.channel, (message) => {\r\n for (const h of this.handlers) {\r\n try {\r\n h(message);\r\n } catch {\r\n /* isolate handlers */\r\n }\r\n }\r\n }),\r\n ).catch(this.onError);\r\n }\r\n return () => this.handlers.delete(handler);\r\n }\r\n}\r\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,kBAA4C;AASrC,IAAM,kBAAN,MAA4C;AAAA,EAChC;AAAA,EACA;AAAA,EAEjB,YAAY,QAAyB,UAAkC,CAAC,GAAG;AACzE,SAAK,SAAS;AACd,SAAK,YAAY,QAAQ,aAAa;AAAA,EACxC;AAAA,EAEQ,IAAI,MAAsB;AAChC,WAAO,GAAG,KAAK,SAAS,GAAG,IAAI;AAAA,EACjC;AAAA,EAEA,MAAM,SAAS,MAAwC;AACrD,UAAM,MAAM,MAAM,KAAK,OAAO,QAAQ,KAAK,IAAI,IAAI,CAAC;AACpD,UAAM,MAAuB,CAAC;AAC9B,eAAW,SAAS,OAAO,OAAO,GAAG,GAAG;AACtC,UAAI;AACJ,UAAI;AACF,iBAAS,KAAK,MAAM,KAAK;AAAA,MAC3B,QAAQ;AACN;AAAA,MACF;AACA,cAAI,4BAAe,MAAM,EAAG,KAAI,KAAK,MAAM;AAAA,IAC7C;AACA,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,IAAI,MAAc,IAAgD;AACtE,UAAM,QAAQ,MAAM,KAAK,OAAO,KAAK,KAAK,IAAI,IAAI,GAAG,EAAE;AACvD,QAAI,SAAS,KAAM,QAAO;AAC1B,QAAI;AACF,YAAM,SAAkB,KAAK,MAAM,KAAK;AACxC,iBAAO,4BAAe,MAAM,IAAI,SAAS;AAAA,IAC3C,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AAAA,EAEA,MAAM,MAAM,MAAc,IAA2B;AACnD,UAAM,MAAM,KAAK,IAAI,IAAI;AACzB,QAAI,GAAG,SAAS;AACd,YAAM,KAAK,OAAO,KAAK,KAAK,GAAG,QAAQ,IAAI,KAAK,UAAU,GAAG,OAAO,CAAC;AAAA,aAC9D,GAAG,SAAS,SAAU,OAAM,KAAK,OAAO,KAAK,KAAK,GAAG,EAAE;AAAA,aACvD,GAAG,SAAS,QAAS,OAAM,KAAK,OAAO,IAAI,GAAG;AAAA,EAEzD;AACF;;;AChDO,IAAM,iBAAN,MAA0C;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,WAAW,oBAAI,IAA+B;AAAA,EACvD,aAAa;AAAA,EAErB,YACE,WACA,YACA,UAAiC,CAAC,GAClC;AACA,SAAK,YAAY;AACjB,SAAK,aAAa;AAClB,SAAK,UAAU,QAAQ,WAAW;AAClC,SAAK,UAAU,QAAQ,YAAY,MAAM;AAAA,EAC3C;AAAA,EAEA,QAAQ,SAAuB;AAC7B,YAAQ,QAAQ,KAAK,UAAU,QAAQ,KAAK,SAAS,OAAO,CAAC,EAAE,MAAM,KAAK,OAAO;AAAA,EACnF;AAAA,EAEA,UAAU,SAAgD;AACxD,SAAK,SAAS,IAAI,OAAO;AACzB,QAAI,CAAC,KAAK,YAAY;AACpB,WAAK,aAAa;AAClB,cAAQ;AAAA,QACN,KAAK,WAAW,UAAU,KAAK,SAAS,CAAC,YAAY;AACnD,qBAAW,KAAK,KAAK,UAAU;AAC7B,gBAAI;AACF,gBAAE,OAAO;AAAA,YACX,QAAQ;AAAA,YAER;AAAA,UACF;AAAA,QACF,CAAC;AAAA,MACH,EAAE,MAAM,KAAK,OAAO;AAAA,IACtB;AACA,WAAO,MAAM,KAAK,SAAS,OAAO,OAAO;AAAA,EAC3C;AACF;","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/redis-hub-backend.ts","../src/redis-hub-fanout.ts"],"sourcesContent":["export { RedisHubBackend } from './redis-hub-backend';\r\nexport type { RedisHubBackendOptions } from './redis-hub-backend';\r\nexport type { RedisHashClient } from './redis-hash-client';\r\nexport { RedisHubFanout } from './redis-hub-fanout';\r\nexport type { RedisHubFanoutOptions } from './redis-hub-fanout';\r\nexport type { RedisPublisher, RedisSubscriber } from './redis-fanout-client';\r\n","import { isValidElement, type SyncOp } from '@fieldnotes/sync';\r\nimport type { CanvasElement } from '@fieldnotes/core';\r\nimport type { HubBackend } from '@fieldnotes/sync-server';\r\nimport type { RedisHashClient } from './redis-hash-client';\r\n\r\nexport interface RedisHubBackendOptions {\r\n keyPrefix?: string; // default 'fieldnotes:room:'\r\n}\r\n\r\nexport class RedisHubBackend implements HubBackend {\r\n private readonly client: RedisHashClient;\r\n private readonly keyPrefix: string;\r\n\r\n constructor(client: RedisHashClient, options: RedisHubBackendOptions = {}) {\r\n this.client = client;\r\n this.keyPrefix = options.keyPrefix ?? 'fieldnotes:room:';\r\n }\r\n\r\n private key(room: string): string {\r\n return `${this.keyPrefix}${room}`;\r\n }\r\n\r\n async snapshot(room: string): Promise<CanvasElement[]> {\r\n const map = await this.client.hGetAll(this.key(room));\r\n const out: CanvasElement[] = [];\r\n for (const value of Object.values(map)) {\r\n let parsed: unknown;\r\n try {\r\n parsed = JSON.parse(value);\r\n } catch {\r\n continue; // skip a corrupt stored value rather than throwing the whole snapshot\r\n }\r\n if (isValidElement(parsed)) out.push(parsed);\r\n }\r\n return out;\r\n }\r\n\r\n async get(room: string, id: string): Promise<CanvasElement | undefined> {\r\n const value = await this.client.hGet(this.key(room), id);\r\n if (value == null) return undefined;\r\n try {\r\n const parsed: unknown = JSON.parse(value);\r\n return isValidElement(parsed) ? parsed : undefined;\r\n } catch {\r\n return undefined;\r\n }\r\n }\r\n\r\n async apply(room: string, op: SyncOp): Promise<void> {\r\n const key = this.key(room);\r\n if (op.kind === 'upsert')\r\n await this.client.hSet(key, op.element.id, JSON.stringify(op.element));\r\n else if (op.kind === 'remove') await this.client.hDel(key, op.id);\r\n else if (op.kind === 'clear') await this.client.del(key);\r\n // request-snapshot/snapshot never reach apply (the hub only applies data ops)\r\n }\r\n}\r\n","import type { HubFanout } from '@fieldnotes/sync-server';\nimport type { RedisPublisher, RedisSubscriber } from './redis-fanout-client';\n\nexport interface RedisHubFanoutOptions {\n channel?: string;\n onError?: (err: unknown) => void;\n}\n\nexport class RedisHubFanout implements HubFanout {\n private readonly publisher: RedisPublisher;\n private readonly subscriber: RedisSubscriber;\n private readonly channel: string;\n private readonly onError: (err: unknown) => void;\n private readonly handlers = new Set<(payload: string) => void>();\n private subscribed = false;\n\n constructor(\n publisher: RedisPublisher,\n subscriber: RedisSubscriber,\n options: RedisHubFanoutOptions = {},\n ) {\n this.publisher = publisher;\n this.subscriber = subscriber;\n this.channel = options.channel ?? 'fieldnotes:fanout';\n this.onError = options.onError ?? (() => undefined);\n }\n\n async publish(payload: string): Promise<void> {\n try {\n await this.publisher.publish(this.channel, payload);\n } catch (error) {\n try {\n this.onError(error);\n } catch {\n /* preserve the publication failure even when the observer throws */\n }\n throw error;\n }\n }\n\n subscribe(handler: (payload: string) => void): () => void {\n this.handlers.add(handler);\n if (!this.subscribed) {\n this.subscribed = true;\n Promise.resolve(\n this.subscriber.subscribe(this.channel, (message) => {\n for (const h of this.handlers) {\n try {\n h(message);\n } catch {\n /* isolate handlers */\n }\n }\n }),\n ).catch(this.onError);\n }\n return () => this.handlers.delete(handler);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,kBAA4C;AASrC,IAAM,kBAAN,MAA4C;AAAA,EAChC;AAAA,EACA;AAAA,EAEjB,YAAY,QAAyB,UAAkC,CAAC,GAAG;AACzE,SAAK,SAAS;AACd,SAAK,YAAY,QAAQ,aAAa;AAAA,EACxC;AAAA,EAEQ,IAAI,MAAsB;AAChC,WAAO,GAAG,KAAK,SAAS,GAAG,IAAI;AAAA,EACjC;AAAA,EAEA,MAAM,SAAS,MAAwC;AACrD,UAAM,MAAM,MAAM,KAAK,OAAO,QAAQ,KAAK,IAAI,IAAI,CAAC;AACpD,UAAM,MAAuB,CAAC;AAC9B,eAAW,SAAS,OAAO,OAAO,GAAG,GAAG;AACtC,UAAI;AACJ,UAAI;AACF,iBAAS,KAAK,MAAM,KAAK;AAAA,MAC3B,QAAQ;AACN;AAAA,MACF;AACA,cAAI,4BAAe,MAAM,EAAG,KAAI,KAAK,MAAM;AAAA,IAC7C;AACA,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,IAAI,MAAc,IAAgD;AACtE,UAAM,QAAQ,MAAM,KAAK,OAAO,KAAK,KAAK,IAAI,IAAI,GAAG,EAAE;AACvD,QAAI,SAAS,KAAM,QAAO;AAC1B,QAAI;AACF,YAAM,SAAkB,KAAK,MAAM,KAAK;AACxC,iBAAO,4BAAe,MAAM,IAAI,SAAS;AAAA,IAC3C,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AAAA,EAEA,MAAM,MAAM,MAAc,IAA2B;AACnD,UAAM,MAAM,KAAK,IAAI,IAAI;AACzB,QAAI,GAAG,SAAS;AACd,YAAM,KAAK,OAAO,KAAK,KAAK,GAAG,QAAQ,IAAI,KAAK,UAAU,GAAG,OAAO,CAAC;AAAA,aAC9D,GAAG,SAAS,SAAU,OAAM,KAAK,OAAO,KAAK,KAAK,GAAG,EAAE;AAAA,aACvD,GAAG,SAAS,QAAS,OAAM,KAAK,OAAO,IAAI,GAAG;AAAA,EAEzD;AACF;;;AChDO,IAAM,iBAAN,MAA0C;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,WAAW,oBAAI,IAA+B;AAAA,EACvD,aAAa;AAAA,EAErB,YACE,WACA,YACA,UAAiC,CAAC,GAClC;AACA,SAAK,YAAY;AACjB,SAAK,aAAa;AAClB,SAAK,UAAU,QAAQ,WAAW;AAClC,SAAK,UAAU,QAAQ,YAAY,MAAM;AAAA,EAC3C;AAAA,EAEA,MAAM,QAAQ,SAAgC;AAC5C,QAAI;AACF,YAAM,KAAK,UAAU,QAAQ,KAAK,SAAS,OAAO;AAAA,IACpD,SAAS,OAAO;AACd,UAAI;AACF,aAAK,QAAQ,KAAK;AAAA,MACpB,QAAQ;AAAA,MAER;AACA,YAAM;AAAA,IACR;AAAA,EACF;AAAA,EAEA,UAAU,SAAgD;AACxD,SAAK,SAAS,IAAI,OAAO;AACzB,QAAI,CAAC,KAAK,YAAY;AACpB,WAAK,aAAa;AAClB,cAAQ;AAAA,QACN,KAAK,WAAW,UAAU,KAAK,SAAS,CAAC,YAAY;AACnD,qBAAW,KAAK,KAAK,UAAU;AAC7B,gBAAI;AACF,gBAAE,OAAO;AAAA,YACX,QAAQ;AAAA,YAER;AAAA,UACF;AAAA,QACF,CAAC;AAAA,MACH,EAAE,MAAM,KAAK,OAAO;AAAA,IACtB;AACA,WAAO,MAAM,KAAK,SAAS,OAAO,OAAO;AAAA,EAC3C;AACF;","names":[]}
package/dist/index.d.cts CHANGED
@@ -42,7 +42,7 @@ declare class RedisHubFanout implements HubFanout {
42
42
  private readonly handlers;
43
43
  private subscribed;
44
44
  constructor(publisher: RedisPublisher, subscriber: RedisSubscriber, options?: RedisHubFanoutOptions);
45
- publish(payload: string): void;
45
+ publish(payload: string): Promise<void>;
46
46
  subscribe(handler: (payload: string) => void): () => void;
47
47
  }
48
48
 
package/dist/index.d.ts CHANGED
@@ -42,7 +42,7 @@ declare class RedisHubFanout implements HubFanout {
42
42
  private readonly handlers;
43
43
  private subscribed;
44
44
  constructor(publisher: RedisPublisher, subscriber: RedisSubscriber, options?: RedisHubFanoutOptions);
45
- publish(payload: string): void;
45
+ publish(payload: string): Promise<void>;
46
46
  subscribe(handler: (payload: string) => void): () => void;
47
47
  }
48
48
 
package/dist/index.js CHANGED
@@ -57,8 +57,16 @@ var RedisHubFanout = class {
57
57
  this.channel = options.channel ?? "fieldnotes:fanout";
58
58
  this.onError = options.onError ?? (() => void 0);
59
59
  }
60
- publish(payload) {
61
- Promise.resolve(this.publisher.publish(this.channel, payload)).catch(this.onError);
60
+ async publish(payload) {
61
+ try {
62
+ await this.publisher.publish(this.channel, payload);
63
+ } catch (error) {
64
+ try {
65
+ this.onError(error);
66
+ } catch {
67
+ }
68
+ throw error;
69
+ }
62
70
  }
63
71
  subscribe(handler) {
64
72
  this.handlers.add(handler);
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/redis-hub-backend.ts","../src/redis-hub-fanout.ts"],"sourcesContent":["import { isValidElement, type SyncOp } from '@fieldnotes/sync';\nimport type { CanvasElement } from '@fieldnotes/core';\nimport type { HubBackend } from '@fieldnotes/sync-server';\nimport type { RedisHashClient } from './redis-hash-client';\n\nexport interface RedisHubBackendOptions {\n keyPrefix?: string; // default 'fieldnotes:room:'\n}\n\nexport class RedisHubBackend implements HubBackend {\n private readonly client: RedisHashClient;\n private readonly keyPrefix: string;\n\n constructor(client: RedisHashClient, options: RedisHubBackendOptions = {}) {\n this.client = client;\n this.keyPrefix = options.keyPrefix ?? 'fieldnotes:room:';\n }\n\n private key(room: string): string {\n return `${this.keyPrefix}${room}`;\n }\n\n async snapshot(room: string): Promise<CanvasElement[]> {\n const map = await this.client.hGetAll(this.key(room));\n const out: CanvasElement[] = [];\n for (const value of Object.values(map)) {\n let parsed: unknown;\n try {\n parsed = JSON.parse(value);\n } catch {\n continue; // skip a corrupt stored value rather than throwing the whole snapshot\n }\n if (isValidElement(parsed)) out.push(parsed);\n }\n return out;\n }\n\n async get(room: string, id: string): Promise<CanvasElement | undefined> {\n const value = await this.client.hGet(this.key(room), id);\n if (value == null) return undefined;\n try {\n const parsed: unknown = JSON.parse(value);\n return isValidElement(parsed) ? parsed : undefined;\n } catch {\n return undefined;\n }\n }\n\n async apply(room: string, op: SyncOp): Promise<void> {\n const key = this.key(room);\n if (op.kind === 'upsert')\n await this.client.hSet(key, op.element.id, JSON.stringify(op.element));\n else if (op.kind === 'remove') await this.client.hDel(key, op.id);\n else if (op.kind === 'clear') await this.client.del(key);\n // request-snapshot/snapshot never reach apply (the hub only applies data ops)\n }\n}\n","import type { HubFanout } from '@fieldnotes/sync-server';\r\nimport type { RedisPublisher, RedisSubscriber } from './redis-fanout-client';\r\n\r\nexport interface RedisHubFanoutOptions {\r\n channel?: string;\r\n onError?: (err: unknown) => void;\r\n}\r\n\r\nexport class RedisHubFanout implements HubFanout {\r\n private readonly publisher: RedisPublisher;\r\n private readonly subscriber: RedisSubscriber;\r\n private readonly channel: string;\r\n private readonly onError: (err: unknown) => void;\r\n private readonly handlers = new Set<(payload: string) => void>();\r\n private subscribed = false;\r\n\r\n constructor(\r\n publisher: RedisPublisher,\r\n subscriber: RedisSubscriber,\r\n options: RedisHubFanoutOptions = {},\r\n ) {\r\n this.publisher = publisher;\r\n this.subscriber = subscriber;\r\n this.channel = options.channel ?? 'fieldnotes:fanout';\r\n this.onError = options.onError ?? (() => undefined);\r\n }\r\n\r\n publish(payload: string): void {\r\n Promise.resolve(this.publisher.publish(this.channel, payload)).catch(this.onError);\r\n }\r\n\r\n subscribe(handler: (payload: string) => void): () => void {\r\n this.handlers.add(handler);\r\n if (!this.subscribed) {\r\n this.subscribed = true;\r\n Promise.resolve(\r\n this.subscriber.subscribe(this.channel, (message) => {\r\n for (const h of this.handlers) {\r\n try {\r\n h(message);\r\n } catch {\r\n /* isolate handlers */\r\n }\r\n }\r\n }),\r\n ).catch(this.onError);\r\n }\r\n return () => this.handlers.delete(handler);\r\n }\r\n}\r\n"],"mappings":";AAAA,SAAS,sBAAmC;AASrC,IAAM,kBAAN,MAA4C;AAAA,EAChC;AAAA,EACA;AAAA,EAEjB,YAAY,QAAyB,UAAkC,CAAC,GAAG;AACzE,SAAK,SAAS;AACd,SAAK,YAAY,QAAQ,aAAa;AAAA,EACxC;AAAA,EAEQ,IAAI,MAAsB;AAChC,WAAO,GAAG,KAAK,SAAS,GAAG,IAAI;AAAA,EACjC;AAAA,EAEA,MAAM,SAAS,MAAwC;AACrD,UAAM,MAAM,MAAM,KAAK,OAAO,QAAQ,KAAK,IAAI,IAAI,CAAC;AACpD,UAAM,MAAuB,CAAC;AAC9B,eAAW,SAAS,OAAO,OAAO,GAAG,GAAG;AACtC,UAAI;AACJ,UAAI;AACF,iBAAS,KAAK,MAAM,KAAK;AAAA,MAC3B,QAAQ;AACN;AAAA,MACF;AACA,UAAI,eAAe,MAAM,EAAG,KAAI,KAAK,MAAM;AAAA,IAC7C;AACA,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,IAAI,MAAc,IAAgD;AACtE,UAAM,QAAQ,MAAM,KAAK,OAAO,KAAK,KAAK,IAAI,IAAI,GAAG,EAAE;AACvD,QAAI,SAAS,KAAM,QAAO;AAC1B,QAAI;AACF,YAAM,SAAkB,KAAK,MAAM,KAAK;AACxC,aAAO,eAAe,MAAM,IAAI,SAAS;AAAA,IAC3C,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AAAA,EAEA,MAAM,MAAM,MAAc,IAA2B;AACnD,UAAM,MAAM,KAAK,IAAI,IAAI;AACzB,QAAI,GAAG,SAAS;AACd,YAAM,KAAK,OAAO,KAAK,KAAK,GAAG,QAAQ,IAAI,KAAK,UAAU,GAAG,OAAO,CAAC;AAAA,aAC9D,GAAG,SAAS,SAAU,OAAM,KAAK,OAAO,KAAK,KAAK,GAAG,EAAE;AAAA,aACvD,GAAG,SAAS,QAAS,OAAM,KAAK,OAAO,IAAI,GAAG;AAAA,EAEzD;AACF;;;AChDO,IAAM,iBAAN,MAA0C;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,WAAW,oBAAI,IAA+B;AAAA,EACvD,aAAa;AAAA,EAErB,YACE,WACA,YACA,UAAiC,CAAC,GAClC;AACA,SAAK,YAAY;AACjB,SAAK,aAAa;AAClB,SAAK,UAAU,QAAQ,WAAW;AAClC,SAAK,UAAU,QAAQ,YAAY,MAAM;AAAA,EAC3C;AAAA,EAEA,QAAQ,SAAuB;AAC7B,YAAQ,QAAQ,KAAK,UAAU,QAAQ,KAAK,SAAS,OAAO,CAAC,EAAE,MAAM,KAAK,OAAO;AAAA,EACnF;AAAA,EAEA,UAAU,SAAgD;AACxD,SAAK,SAAS,IAAI,OAAO;AACzB,QAAI,CAAC,KAAK,YAAY;AACpB,WAAK,aAAa;AAClB,cAAQ;AAAA,QACN,KAAK,WAAW,UAAU,KAAK,SAAS,CAAC,YAAY;AACnD,qBAAW,KAAK,KAAK,UAAU;AAC7B,gBAAI;AACF,gBAAE,OAAO;AAAA,YACX,QAAQ;AAAA,YAER;AAAA,UACF;AAAA,QACF,CAAC;AAAA,MACH,EAAE,MAAM,KAAK,OAAO;AAAA,IACtB;AACA,WAAO,MAAM,KAAK,SAAS,OAAO,OAAO;AAAA,EAC3C;AACF;","names":[]}
1
+ {"version":3,"sources":["../src/redis-hub-backend.ts","../src/redis-hub-fanout.ts"],"sourcesContent":["import { isValidElement, type SyncOp } from '@fieldnotes/sync';\r\nimport type { CanvasElement } from '@fieldnotes/core';\r\nimport type { HubBackend } from '@fieldnotes/sync-server';\r\nimport type { RedisHashClient } from './redis-hash-client';\r\n\r\nexport interface RedisHubBackendOptions {\r\n keyPrefix?: string; // default 'fieldnotes:room:'\r\n}\r\n\r\nexport class RedisHubBackend implements HubBackend {\r\n private readonly client: RedisHashClient;\r\n private readonly keyPrefix: string;\r\n\r\n constructor(client: RedisHashClient, options: RedisHubBackendOptions = {}) {\r\n this.client = client;\r\n this.keyPrefix = options.keyPrefix ?? 'fieldnotes:room:';\r\n }\r\n\r\n private key(room: string): string {\r\n return `${this.keyPrefix}${room}`;\r\n }\r\n\r\n async snapshot(room: string): Promise<CanvasElement[]> {\r\n const map = await this.client.hGetAll(this.key(room));\r\n const out: CanvasElement[] = [];\r\n for (const value of Object.values(map)) {\r\n let parsed: unknown;\r\n try {\r\n parsed = JSON.parse(value);\r\n } catch {\r\n continue; // skip a corrupt stored value rather than throwing the whole snapshot\r\n }\r\n if (isValidElement(parsed)) out.push(parsed);\r\n }\r\n return out;\r\n }\r\n\r\n async get(room: string, id: string): Promise<CanvasElement | undefined> {\r\n const value = await this.client.hGet(this.key(room), id);\r\n if (value == null) return undefined;\r\n try {\r\n const parsed: unknown = JSON.parse(value);\r\n return isValidElement(parsed) ? parsed : undefined;\r\n } catch {\r\n return undefined;\r\n }\r\n }\r\n\r\n async apply(room: string, op: SyncOp): Promise<void> {\r\n const key = this.key(room);\r\n if (op.kind === 'upsert')\r\n await this.client.hSet(key, op.element.id, JSON.stringify(op.element));\r\n else if (op.kind === 'remove') await this.client.hDel(key, op.id);\r\n else if (op.kind === 'clear') await this.client.del(key);\r\n // request-snapshot/snapshot never reach apply (the hub only applies data ops)\r\n }\r\n}\r\n","import type { HubFanout } from '@fieldnotes/sync-server';\nimport type { RedisPublisher, RedisSubscriber } from './redis-fanout-client';\n\nexport interface RedisHubFanoutOptions {\n channel?: string;\n onError?: (err: unknown) => void;\n}\n\nexport class RedisHubFanout implements HubFanout {\n private readonly publisher: RedisPublisher;\n private readonly subscriber: RedisSubscriber;\n private readonly channel: string;\n private readonly onError: (err: unknown) => void;\n private readonly handlers = new Set<(payload: string) => void>();\n private subscribed = false;\n\n constructor(\n publisher: RedisPublisher,\n subscriber: RedisSubscriber,\n options: RedisHubFanoutOptions = {},\n ) {\n this.publisher = publisher;\n this.subscriber = subscriber;\n this.channel = options.channel ?? 'fieldnotes:fanout';\n this.onError = options.onError ?? (() => undefined);\n }\n\n async publish(payload: string): Promise<void> {\n try {\n await this.publisher.publish(this.channel, payload);\n } catch (error) {\n try {\n this.onError(error);\n } catch {\n /* preserve the publication failure even when the observer throws */\n }\n throw error;\n }\n }\n\n subscribe(handler: (payload: string) => void): () => void {\n this.handlers.add(handler);\n if (!this.subscribed) {\n this.subscribed = true;\n Promise.resolve(\n this.subscriber.subscribe(this.channel, (message) => {\n for (const h of this.handlers) {\n try {\n h(message);\n } catch {\n /* isolate handlers */\n }\n }\n }),\n ).catch(this.onError);\n }\n return () => this.handlers.delete(handler);\n }\n}\n"],"mappings":";AAAA,SAAS,sBAAmC;AASrC,IAAM,kBAAN,MAA4C;AAAA,EAChC;AAAA,EACA;AAAA,EAEjB,YAAY,QAAyB,UAAkC,CAAC,GAAG;AACzE,SAAK,SAAS;AACd,SAAK,YAAY,QAAQ,aAAa;AAAA,EACxC;AAAA,EAEQ,IAAI,MAAsB;AAChC,WAAO,GAAG,KAAK,SAAS,GAAG,IAAI;AAAA,EACjC;AAAA,EAEA,MAAM,SAAS,MAAwC;AACrD,UAAM,MAAM,MAAM,KAAK,OAAO,QAAQ,KAAK,IAAI,IAAI,CAAC;AACpD,UAAM,MAAuB,CAAC;AAC9B,eAAW,SAAS,OAAO,OAAO,GAAG,GAAG;AACtC,UAAI;AACJ,UAAI;AACF,iBAAS,KAAK,MAAM,KAAK;AAAA,MAC3B,QAAQ;AACN;AAAA,MACF;AACA,UAAI,eAAe,MAAM,EAAG,KAAI,KAAK,MAAM;AAAA,IAC7C;AACA,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,IAAI,MAAc,IAAgD;AACtE,UAAM,QAAQ,MAAM,KAAK,OAAO,KAAK,KAAK,IAAI,IAAI,GAAG,EAAE;AACvD,QAAI,SAAS,KAAM,QAAO;AAC1B,QAAI;AACF,YAAM,SAAkB,KAAK,MAAM,KAAK;AACxC,aAAO,eAAe,MAAM,IAAI,SAAS;AAAA,IAC3C,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AAAA,EAEA,MAAM,MAAM,MAAc,IAA2B;AACnD,UAAM,MAAM,KAAK,IAAI,IAAI;AACzB,QAAI,GAAG,SAAS;AACd,YAAM,KAAK,OAAO,KAAK,KAAK,GAAG,QAAQ,IAAI,KAAK,UAAU,GAAG,OAAO,CAAC;AAAA,aAC9D,GAAG,SAAS,SAAU,OAAM,KAAK,OAAO,KAAK,KAAK,GAAG,EAAE;AAAA,aACvD,GAAG,SAAS,QAAS,OAAM,KAAK,OAAO,IAAI,GAAG;AAAA,EAEzD;AACF;;;AChDO,IAAM,iBAAN,MAA0C;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,WAAW,oBAAI,IAA+B;AAAA,EACvD,aAAa;AAAA,EAErB,YACE,WACA,YACA,UAAiC,CAAC,GAClC;AACA,SAAK,YAAY;AACjB,SAAK,aAAa;AAClB,SAAK,UAAU,QAAQ,WAAW;AAClC,SAAK,UAAU,QAAQ,YAAY,MAAM;AAAA,EAC3C;AAAA,EAEA,MAAM,QAAQ,SAAgC;AAC5C,QAAI;AACF,YAAM,KAAK,UAAU,QAAQ,KAAK,SAAS,OAAO;AAAA,IACpD,SAAS,OAAO;AACd,UAAI;AACF,aAAK,QAAQ,KAAK;AAAA,MACpB,QAAQ;AAAA,MAER;AACA,YAAM;AAAA,IACR;AAAA,EACF;AAAA,EAEA,UAAU,SAAgD;AACxD,SAAK,SAAS,IAAI,OAAO;AACzB,QAAI,CAAC,KAAK,YAAY;AACpB,WAAK,aAAa;AAClB,cAAQ;AAAA,QACN,KAAK,WAAW,UAAU,KAAK,SAAS,CAAC,YAAY;AACnD,qBAAW,KAAK,KAAK,UAAU;AAC7B,gBAAI;AACF,gBAAE,OAAO;AAAA,YACX,QAAQ;AAAA,YAER;AAAA,UACF;AAAA,QACF,CAAC;AAAA,MACH,EAAE,MAAM,KAAK,OAAO;AAAA,IACtB;AACA,WAAO,MAAM,KAAK,SAAS,OAAO,OAAO;AAAA,EAC3C;AACF;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fieldnotes/sync-redis",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "Redis-backed HubBackend for Field Notes real-time sync relay",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -36,14 +36,14 @@
36
36
  "fieldnotes"
37
37
  ],
38
38
  "dependencies": {
39
- "@fieldnotes/sync": "0.5.0"
39
+ "@fieldnotes/sync": "0.7.1"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@vitest/coverage-v8": "^4.1.0",
43
43
  "tsup": "^8.5.1",
44
44
  "vitest": "^4.1.0",
45
- "@fieldnotes/core": "0.46.0",
46
- "@fieldnotes/sync-server": "0.4.0"
45
+ "@fieldnotes/sync-server": "0.10.1",
46
+ "@fieldnotes/core": "0.52.2"
47
47
  },
48
48
  "scripts": {
49
49
  "build": "tsup",