@fieldnotes/sync-redis 0.8.1 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -21,12 +21,20 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
23
  RedisHubBackend: () => RedisHubBackend,
24
- RedisHubFanout: () => RedisHubFanout
24
+ RedisHubFanout: () => RedisHubFanout,
25
+ encodeRoomKey: () => encodeRoomKey
25
26
  });
26
27
  module.exports = __toCommonJS(index_exports);
27
28
 
28
29
  // src/redis-hub-backend.ts
29
30
  var import_sync = require("@fieldnotes/sync");
31
+
32
+ // src/room-key.ts
33
+ function encodeRoomKey(room) {
34
+ return encodeURIComponent(room);
35
+ }
36
+
37
+ // src/redis-hub-backend.ts
30
38
  var RedisHubBackend = class {
31
39
  sharedAcrossInstances = true;
32
40
  client;
@@ -42,10 +50,10 @@ var RedisHubBackend = class {
42
50
  return this.services.get(key.id);
43
51
  }
44
52
  key(room) {
45
- return `${this.keyPrefix}${room}`;
53
+ return `${this.keyPrefix}${encodeRoomKey(room)}`;
46
54
  }
47
55
  layersKey(room) {
48
- return `${this.keyPrefix}${room}:layers`;
56
+ return `${this.key(room)}:layers`;
49
57
  }
50
58
  async snapshot(room) {
51
59
  const map = await this.client.hGetAll(this.key(room));
@@ -53,7 +61,7 @@ var RedisHubBackend = class {
53
61
  for (const value of Object.values(map)) {
54
62
  try {
55
63
  const parsed = JSON.parse(value);
56
- if ((0, import_sync.isValidElement)(parsed)) out.push(parsed);
64
+ if ((0, import_sync.isValidWireElement)(parsed)) out.push(parsed);
57
65
  } catch {
58
66
  }
59
67
  }
@@ -64,7 +72,7 @@ var RedisHubBackend = class {
64
72
  if (value == null) return void 0;
65
73
  try {
66
74
  const parsed = JSON.parse(value);
67
- return (0, import_sync.isValidElement)(parsed) ? parsed : void 0;
75
+ return (0, import_sync.isValidWireElement)(parsed) ? parsed : void 0;
68
76
  } catch {
69
77
  return void 0;
70
78
  }
@@ -124,6 +132,7 @@ var RedisHubBackend = class {
124
132
  plugin.start({
125
133
  client: this.client,
126
134
  roomKeyPrefix: this.keyPrefix,
135
+ roomKey: (room) => this.key(room),
127
136
  registerService: (key, service) => {
128
137
  if (this.services.has(key.id)) {
129
138
  throw new Error(`Backend service "${key.name}" is already registered`);
@@ -202,6 +211,7 @@ var RedisHubFanout = class {
202
211
  // Annotate the CommonJS export names for ESM import in node:
203
212
  0 && (module.exports = {
204
213
  RedisHubBackend,
205
- RedisHubFanout
214
+ RedisHubFanout,
215
+ encodeRoomKey
206
216
  });
207
217
  //# sourceMappingURL=index.cjs.map
@@ -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';\nexport type { RedisHubBackendOptions } from './redis-hub-backend';\nexport type { RedisHashClient } from './redis-hash-client';\nexport { RedisHubFanout } from './redis-hub-fanout';\nexport type { RedisHubFanoutOptions } from './redis-hub-fanout';\nexport type { RedisPublisher, RedisSubscriber } from './redis-fanout-client';\nexport type { BackendSyncPlugin, BackendPluginContext } from './sync-plugin';\n","import type { CanvasElement, ServiceKey } from '@fieldnotes/core';\nimport {\n isValidElement,\n isValidLayerRecord,\n type LayerRecord,\n type SyncOp,\n} from '@fieldnotes/sync';\nimport type { HubBackend } from '@fieldnotes/sync-server';\nimport type { RedisHashClient } from './redis-hash-client';\nimport type { BackendSyncPlugin } from './sync-plugin';\n\nexport interface RedisHubBackendOptions {\n keyPrefix?: string;\n plugins?: readonly BackendSyncPlugin[];\n}\n\nexport class RedisHubBackend implements HubBackend {\n readonly sharedAcrossInstances = true;\n private readonly client: RedisHashClient;\n private readonly keyPrefix: string;\n private readonly services = new Map<symbol, unknown>();\n private readonly pluginDisposers: (() => void)[] = [];\n\n constructor(client: RedisHashClient, options: RedisHubBackendOptions = {}) {\n this.client = client;\n this.keyPrefix = options.keyPrefix ?? 'fieldnotes:room:';\n this.installPlugins(options.plugins ?? []);\n }\n\n getService<T>(key: ServiceKey<T>): T | undefined {\n return this.services.get(key.id) as T | undefined;\n }\n\n private key(room: string): string {\n return `${this.keyPrefix}${room}`;\n }\n\n private layersKey(room: string): string {\n return `${this.keyPrefix}${room}:layers`;\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 try {\n const parsed: unknown = JSON.parse(value);\n if (isValidElement(parsed)) out.push(parsed);\n } catch {\n // Corrupt fields are isolated from the rest of the room snapshot.\n }\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 }\n\n async layerRecords(room: string): Promise<LayerRecord[]> {\n const map = await this.client.hGetAll(this.layersKey(room));\n const out: LayerRecord[] = [];\n for (const value of Object.values(map)) {\n try {\n const parsed: unknown = JSON.parse(value);\n if (isValidLayerRecord(parsed)) out.push(parsed);\n } catch {\n // Corrupt fields are isolated from the rest of the layer ledger.\n }\n }\n return out;\n }\n\n async getLayerRecord(room: string, id: string): Promise<LayerRecord | undefined> {\n const value = await this.client.hGet(this.layersKey(room), id);\n if (value == null) return undefined;\n try {\n const parsed: unknown = JSON.parse(value);\n return isValidLayerRecord(parsed) ? parsed : undefined;\n } catch {\n return undefined;\n }\n }\n\n async applyLayerRecord(room: string, record: LayerRecord): Promise<void> {\n await this.client.hSet(this.layersKey(room), record.id, JSON.stringify(record));\n }\n\n dispose(): void {\n for (const dispose of [...this.pluginDisposers].reverse()) safelyDispose(dispose);\n this.pluginDisposers.length = 0;\n this.services.clear();\n }\n\n private installPlugins(plugins: readonly BackendSyncPlugin[]): void {\n const names = new Set<string>();\n const prefixes = new Set<string>();\n try {\n for (const plugin of plugins) {\n if (names.has(plugin.name))\n throw new Error(`Backend plugin \"${plugin.name}\" is duplicated`);\n if (prefixes.has(plugin.keyPrefix)) {\n throw new Error(`Backend plugin key prefix \"${plugin.keyPrefix}\" is duplicated`);\n }\n names.add(plugin.name);\n prefixes.add(plugin.keyPrefix);\n const localDisposers: (() => void)[] = [];\n const serviceKeys: symbol[] = [];\n try {\n plugin.start({\n client: this.client,\n roomKeyPrefix: this.keyPrefix,\n registerService: (key, service) => {\n if (this.services.has(key.id)) {\n throw new Error(`Backend service \"${key.name}\" is already registered`);\n }\n this.services.set(key.id, service);\n serviceKeys.push(key.id);\n },\n addDisposer: (dispose) => localDisposers.push(dispose),\n });\n } catch (error) {\n for (const dispose of localDisposers.reverse()) safelyDispose(dispose);\n for (const key of serviceKeys) this.services.delete(key);\n throw error;\n }\n this.pluginDisposers.push(() => {\n for (const dispose of localDisposers.reverse()) safelyDispose(dispose);\n for (const key of serviceKeys) this.services.delete(key);\n });\n }\n } catch (error) {\n this.dispose();\n throw error;\n }\n }\n}\n\nfunction safelyDispose(dispose: () => void): void {\n try {\n dispose();\n } catch {\n // Continue rolling back later resources.\n }\n}\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":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/redis-hub-backend.ts","../src/room-key.ts","../src/redis-hub-fanout.ts"],"sourcesContent":["export { RedisHubBackend } from './redis-hub-backend';\nexport type { RedisHubBackendOptions } from './redis-hub-backend';\nexport type { RedisHashClient } from './redis-hash-client';\nexport { RedisHubFanout } from './redis-hub-fanout';\nexport type { RedisHubFanoutOptions } from './redis-hub-fanout';\nexport type { RedisPublisher, RedisSubscriber } from './redis-fanout-client';\nexport type { BackendSyncPlugin, BackendPluginContext } from './sync-plugin';\nexport { encodeRoomKey } from './room-key';\n","import type { ServiceKey } from '@fieldnotes/core';\nimport {\n isValidWireElement,\n isValidLayerRecord,\n type LayerRecord,\n type WireSyncElement,\n type WireSyncOp,\n} from '@fieldnotes/sync';\nimport type { HubBackend } from '@fieldnotes/sync-server';\nimport type { RedisHashClient } from './redis-hash-client';\nimport type { BackendSyncPlugin } from './sync-plugin';\nimport { encodeRoomKey } from './room-key';\n\nexport interface RedisHubBackendOptions {\n keyPrefix?: string;\n plugins?: readonly BackendSyncPlugin[];\n}\n\nexport class RedisHubBackend implements HubBackend {\n readonly sharedAcrossInstances = true;\n private readonly client: RedisHashClient;\n private readonly keyPrefix: string;\n private readonly services = new Map<symbol, unknown>();\n private readonly pluginDisposers: (() => void)[] = [];\n\n constructor(client: RedisHashClient, options: RedisHubBackendOptions = {}) {\n this.client = client;\n this.keyPrefix = options.keyPrefix ?? 'fieldnotes:room:';\n this.installPlugins(options.plugins ?? []);\n }\n\n getService<T>(key: ServiceKey<T>): T | undefined {\n return this.services.get(key.id) as T | undefined;\n }\n\n private key(room: string): string {\n return `${this.keyPrefix}${encodeRoomKey(room)}`;\n }\n\n private layersKey(room: string): string {\n return `${this.key(room)}:layers`;\n }\n\n async snapshot(room: string): Promise<WireSyncElement[]> {\n const map = await this.client.hGetAll(this.key(room));\n const out: WireSyncElement[] = [];\n for (const value of Object.values(map)) {\n try {\n const parsed: unknown = JSON.parse(value);\n if (isValidWireElement(parsed)) out.push(parsed);\n } catch {\n // Corrupt fields are isolated from the rest of the room snapshot.\n }\n }\n return out;\n }\n\n async get(room: string, id: string): Promise<WireSyncElement | 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 isValidWireElement(parsed) ? parsed : undefined;\n } catch {\n return undefined;\n }\n }\n\n async apply(room: string, op: WireSyncOp): 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 }\n\n async layerRecords(room: string): Promise<LayerRecord[]> {\n const map = await this.client.hGetAll(this.layersKey(room));\n const out: LayerRecord[] = [];\n for (const value of Object.values(map)) {\n try {\n const parsed: unknown = JSON.parse(value);\n if (isValidLayerRecord(parsed)) out.push(parsed);\n } catch {\n // Corrupt fields are isolated from the rest of the layer ledger.\n }\n }\n return out;\n }\n\n async getLayerRecord(room: string, id: string): Promise<LayerRecord | undefined> {\n const value = await this.client.hGet(this.layersKey(room), id);\n if (value == null) return undefined;\n try {\n const parsed: unknown = JSON.parse(value);\n return isValidLayerRecord(parsed) ? parsed : undefined;\n } catch {\n return undefined;\n }\n }\n\n async applyLayerRecord(room: string, record: LayerRecord): Promise<void> {\n await this.client.hSet(this.layersKey(room), record.id, JSON.stringify(record));\n }\n\n dispose(): void {\n for (const dispose of [...this.pluginDisposers].reverse()) safelyDispose(dispose);\n this.pluginDisposers.length = 0;\n this.services.clear();\n }\n\n private installPlugins(plugins: readonly BackendSyncPlugin[]): void {\n const names = new Set<string>();\n const prefixes = new Set<string>();\n try {\n for (const plugin of plugins) {\n if (names.has(plugin.name))\n throw new Error(`Backend plugin \"${plugin.name}\" is duplicated`);\n if (prefixes.has(plugin.keyPrefix)) {\n throw new Error(`Backend plugin key prefix \"${plugin.keyPrefix}\" is duplicated`);\n }\n names.add(plugin.name);\n prefixes.add(plugin.keyPrefix);\n const localDisposers: (() => void)[] = [];\n const serviceKeys: symbol[] = [];\n try {\n plugin.start({\n client: this.client,\n roomKeyPrefix: this.keyPrefix,\n roomKey: (room) => this.key(room),\n registerService: (key, service) => {\n if (this.services.has(key.id)) {\n throw new Error(`Backend service \"${key.name}\" is already registered`);\n }\n this.services.set(key.id, service);\n serviceKeys.push(key.id);\n },\n addDisposer: (dispose) => localDisposers.push(dispose),\n });\n } catch (error) {\n for (const dispose of localDisposers.reverse()) safelyDispose(dispose);\n for (const key of serviceKeys) this.services.delete(key);\n throw error;\n }\n this.pluginDisposers.push(() => {\n for (const dispose of localDisposers.reverse()) safelyDispose(dispose);\n for (const key of serviceKeys) this.services.delete(key);\n });\n }\n } catch (error) {\n this.dispose();\n throw error;\n }\n }\n}\n\nfunction safelyDispose(dispose: () => void): void {\n try {\n dispose();\n } catch {\n // Continue rolling back later resources.\n }\n}\n","/**\n * Encodes a room name for use inside a Redis key. Room-scoped hashes are laid\n * out as `<prefix><room>` plus `<prefix><room>:<suffix>` sub-keys, so an\n * unescaped `:` in a room name lets `foo:layers` alias room `foo`'s layer\n * ledger. `encodeURIComponent` leaves the relay's valid room alphabet\n * (`[A-Za-z0-9_-]`) untouched, so existing keys keep their historical layout.\n */\nexport function encodeRoomKey(room: string): string {\n return encodeURIComponent(room);\n}\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":"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","names":[]}
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
- import { ServiceKey, CanvasElement } from '@fieldnotes/core';
2
- import { SyncOp, LayerRecord } from '@fieldnotes/sync';
1
+ import { ServiceKey } from '@fieldnotes/core';
2
+ import { WireSyncElement, WireSyncOp, LayerRecord } from '@fieldnotes/sync';
3
3
  import { HubBackend, HubFanout } from '@fieldnotes/sync-server';
4
4
 
5
5
  interface RedisHashClient {
@@ -17,7 +17,14 @@ interface RedisHashClient {
17
17
 
18
18
  interface BackendPluginContext {
19
19
  readonly client: RedisHashClient;
20
+ /** Raw key prefix shared by every room-scoped hash. Prefer `roomKey`. */
20
21
  readonly roomKeyPrefix: string;
22
+ /**
23
+ * The escaped base key for `room`; derive sub-keys as `${roomKey(room)}:<suffix>`.
24
+ * Building keys from `roomKeyPrefix` and the raw room name lets a hostile
25
+ * room name alias another room's sub-key hashes.
26
+ */
27
+ roomKey(room: string): string;
21
28
  registerService<T>(key: ServiceKey<T>, service: NoInfer<T>): void;
22
29
  addDisposer(dispose: () => void): void;
23
30
  }
@@ -42,9 +49,9 @@ declare class RedisHubBackend implements HubBackend {
42
49
  getService<T>(key: ServiceKey<T>): T | undefined;
43
50
  private key;
44
51
  private layersKey;
45
- snapshot(room: string): Promise<CanvasElement[]>;
46
- get(room: string, id: string): Promise<CanvasElement | undefined>;
47
- apply(room: string, op: SyncOp): Promise<void>;
52
+ snapshot(room: string): Promise<WireSyncElement[]>;
53
+ get(room: string, id: string): Promise<WireSyncElement | undefined>;
54
+ apply(room: string, op: WireSyncOp): Promise<void>;
48
55
  layerRecords(room: string): Promise<LayerRecord[]>;
49
56
  getLayerRecord(room: string, id: string): Promise<LayerRecord | undefined>;
50
57
  applyLayerRecord(room: string, record: LayerRecord): Promise<void>;
@@ -75,4 +82,13 @@ declare class RedisHubFanout implements HubFanout {
75
82
  subscribe(handler: (payload: string) => void): () => void;
76
83
  }
77
84
 
78
- export { type BackendPluginContext, type BackendSyncPlugin, type RedisHashClient, RedisHubBackend, type RedisHubBackendOptions, RedisHubFanout, type RedisHubFanoutOptions, type RedisPublisher, type RedisSubscriber };
85
+ /**
86
+ * Encodes a room name for use inside a Redis key. Room-scoped hashes are laid
87
+ * out as `<prefix><room>` plus `<prefix><room>:<suffix>` sub-keys, so an
88
+ * unescaped `:` in a room name lets `foo:layers` alias room `foo`'s layer
89
+ * ledger. `encodeURIComponent` leaves the relay's valid room alphabet
90
+ * (`[A-Za-z0-9_-]`) untouched, so existing keys keep their historical layout.
91
+ */
92
+ declare function encodeRoomKey(room: string): string;
93
+
94
+ export { type BackendPluginContext, type BackendSyncPlugin, type RedisHashClient, RedisHubBackend, type RedisHubBackendOptions, RedisHubFanout, type RedisHubFanoutOptions, type RedisPublisher, type RedisSubscriber, encodeRoomKey };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { ServiceKey, CanvasElement } from '@fieldnotes/core';
2
- import { SyncOp, LayerRecord } from '@fieldnotes/sync';
1
+ import { ServiceKey } from '@fieldnotes/core';
2
+ import { WireSyncElement, WireSyncOp, LayerRecord } from '@fieldnotes/sync';
3
3
  import { HubBackend, HubFanout } from '@fieldnotes/sync-server';
4
4
 
5
5
  interface RedisHashClient {
@@ -17,7 +17,14 @@ interface RedisHashClient {
17
17
 
18
18
  interface BackendPluginContext {
19
19
  readonly client: RedisHashClient;
20
+ /** Raw key prefix shared by every room-scoped hash. Prefer `roomKey`. */
20
21
  readonly roomKeyPrefix: string;
22
+ /**
23
+ * The escaped base key for `room`; derive sub-keys as `${roomKey(room)}:<suffix>`.
24
+ * Building keys from `roomKeyPrefix` and the raw room name lets a hostile
25
+ * room name alias another room's sub-key hashes.
26
+ */
27
+ roomKey(room: string): string;
21
28
  registerService<T>(key: ServiceKey<T>, service: NoInfer<T>): void;
22
29
  addDisposer(dispose: () => void): void;
23
30
  }
@@ -42,9 +49,9 @@ declare class RedisHubBackend implements HubBackend {
42
49
  getService<T>(key: ServiceKey<T>): T | undefined;
43
50
  private key;
44
51
  private layersKey;
45
- snapshot(room: string): Promise<CanvasElement[]>;
46
- get(room: string, id: string): Promise<CanvasElement | undefined>;
47
- apply(room: string, op: SyncOp): Promise<void>;
52
+ snapshot(room: string): Promise<WireSyncElement[]>;
53
+ get(room: string, id: string): Promise<WireSyncElement | undefined>;
54
+ apply(room: string, op: WireSyncOp): Promise<void>;
48
55
  layerRecords(room: string): Promise<LayerRecord[]>;
49
56
  getLayerRecord(room: string, id: string): Promise<LayerRecord | undefined>;
50
57
  applyLayerRecord(room: string, record: LayerRecord): Promise<void>;
@@ -75,4 +82,13 @@ declare class RedisHubFanout implements HubFanout {
75
82
  subscribe(handler: (payload: string) => void): () => void;
76
83
  }
77
84
 
78
- export { type BackendPluginContext, type BackendSyncPlugin, type RedisHashClient, RedisHubBackend, type RedisHubBackendOptions, RedisHubFanout, type RedisHubFanoutOptions, type RedisPublisher, type RedisSubscriber };
85
+ /**
86
+ * Encodes a room name for use inside a Redis key. Room-scoped hashes are laid
87
+ * out as `<prefix><room>` plus `<prefix><room>:<suffix>` sub-keys, so an
88
+ * unescaped `:` in a room name lets `foo:layers` alias room `foo`'s layer
89
+ * ledger. `encodeURIComponent` leaves the relay's valid room alphabet
90
+ * (`[A-Za-z0-9_-]`) untouched, so existing keys keep their historical layout.
91
+ */
92
+ declare function encodeRoomKey(room: string): string;
93
+
94
+ export { type BackendPluginContext, type BackendSyncPlugin, type RedisHashClient, RedisHubBackend, type RedisHubBackendOptions, RedisHubFanout, type RedisHubFanoutOptions, type RedisPublisher, type RedisSubscriber, encodeRoomKey };
package/dist/index.js CHANGED
@@ -1,8 +1,15 @@
1
1
  // src/redis-hub-backend.ts
2
2
  import {
3
- isValidElement,
3
+ isValidWireElement,
4
4
  isValidLayerRecord
5
5
  } from "@fieldnotes/sync";
6
+
7
+ // src/room-key.ts
8
+ function encodeRoomKey(room) {
9
+ return encodeURIComponent(room);
10
+ }
11
+
12
+ // src/redis-hub-backend.ts
6
13
  var RedisHubBackend = class {
7
14
  sharedAcrossInstances = true;
8
15
  client;
@@ -18,10 +25,10 @@ var RedisHubBackend = class {
18
25
  return this.services.get(key.id);
19
26
  }
20
27
  key(room) {
21
- return `${this.keyPrefix}${room}`;
28
+ return `${this.keyPrefix}${encodeRoomKey(room)}`;
22
29
  }
23
30
  layersKey(room) {
24
- return `${this.keyPrefix}${room}:layers`;
31
+ return `${this.key(room)}:layers`;
25
32
  }
26
33
  async snapshot(room) {
27
34
  const map = await this.client.hGetAll(this.key(room));
@@ -29,7 +36,7 @@ var RedisHubBackend = class {
29
36
  for (const value of Object.values(map)) {
30
37
  try {
31
38
  const parsed = JSON.parse(value);
32
- if (isValidElement(parsed)) out.push(parsed);
39
+ if (isValidWireElement(parsed)) out.push(parsed);
33
40
  } catch {
34
41
  }
35
42
  }
@@ -40,7 +47,7 @@ var RedisHubBackend = class {
40
47
  if (value == null) return void 0;
41
48
  try {
42
49
  const parsed = JSON.parse(value);
43
- return isValidElement(parsed) ? parsed : void 0;
50
+ return isValidWireElement(parsed) ? parsed : void 0;
44
51
  } catch {
45
52
  return void 0;
46
53
  }
@@ -100,6 +107,7 @@ var RedisHubBackend = class {
100
107
  plugin.start({
101
108
  client: this.client,
102
109
  roomKeyPrefix: this.keyPrefix,
110
+ roomKey: (room) => this.key(room),
103
111
  registerService: (key, service) => {
104
112
  if (this.services.has(key.id)) {
105
113
  throw new Error(`Backend service "${key.name}" is already registered`);
@@ -177,6 +185,7 @@ var RedisHubFanout = class {
177
185
  };
178
186
  export {
179
187
  RedisHubBackend,
180
- RedisHubFanout
188
+ RedisHubFanout,
189
+ encodeRoomKey
181
190
  };
182
191
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/redis-hub-backend.ts","../src/redis-hub-fanout.ts"],"sourcesContent":["import type { CanvasElement, ServiceKey } from '@fieldnotes/core';\nimport {\n isValidElement,\n isValidLayerRecord,\n type LayerRecord,\n type SyncOp,\n} from '@fieldnotes/sync';\nimport type { HubBackend } from '@fieldnotes/sync-server';\nimport type { RedisHashClient } from './redis-hash-client';\nimport type { BackendSyncPlugin } from './sync-plugin';\n\nexport interface RedisHubBackendOptions {\n keyPrefix?: string;\n plugins?: readonly BackendSyncPlugin[];\n}\n\nexport class RedisHubBackend implements HubBackend {\n readonly sharedAcrossInstances = true;\n private readonly client: RedisHashClient;\n private readonly keyPrefix: string;\n private readonly services = new Map<symbol, unknown>();\n private readonly pluginDisposers: (() => void)[] = [];\n\n constructor(client: RedisHashClient, options: RedisHubBackendOptions = {}) {\n this.client = client;\n this.keyPrefix = options.keyPrefix ?? 'fieldnotes:room:';\n this.installPlugins(options.plugins ?? []);\n }\n\n getService<T>(key: ServiceKey<T>): T | undefined {\n return this.services.get(key.id) as T | undefined;\n }\n\n private key(room: string): string {\n return `${this.keyPrefix}${room}`;\n }\n\n private layersKey(room: string): string {\n return `${this.keyPrefix}${room}:layers`;\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 try {\n const parsed: unknown = JSON.parse(value);\n if (isValidElement(parsed)) out.push(parsed);\n } catch {\n // Corrupt fields are isolated from the rest of the room snapshot.\n }\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 }\n\n async layerRecords(room: string): Promise<LayerRecord[]> {\n const map = await this.client.hGetAll(this.layersKey(room));\n const out: LayerRecord[] = [];\n for (const value of Object.values(map)) {\n try {\n const parsed: unknown = JSON.parse(value);\n if (isValidLayerRecord(parsed)) out.push(parsed);\n } catch {\n // Corrupt fields are isolated from the rest of the layer ledger.\n }\n }\n return out;\n }\n\n async getLayerRecord(room: string, id: string): Promise<LayerRecord | undefined> {\n const value = await this.client.hGet(this.layersKey(room), id);\n if (value == null) return undefined;\n try {\n const parsed: unknown = JSON.parse(value);\n return isValidLayerRecord(parsed) ? parsed : undefined;\n } catch {\n return undefined;\n }\n }\n\n async applyLayerRecord(room: string, record: LayerRecord): Promise<void> {\n await this.client.hSet(this.layersKey(room), record.id, JSON.stringify(record));\n }\n\n dispose(): void {\n for (const dispose of [...this.pluginDisposers].reverse()) safelyDispose(dispose);\n this.pluginDisposers.length = 0;\n this.services.clear();\n }\n\n private installPlugins(plugins: readonly BackendSyncPlugin[]): void {\n const names = new Set<string>();\n const prefixes = new Set<string>();\n try {\n for (const plugin of plugins) {\n if (names.has(plugin.name))\n throw new Error(`Backend plugin \"${plugin.name}\" is duplicated`);\n if (prefixes.has(plugin.keyPrefix)) {\n throw new Error(`Backend plugin key prefix \"${plugin.keyPrefix}\" is duplicated`);\n }\n names.add(plugin.name);\n prefixes.add(plugin.keyPrefix);\n const localDisposers: (() => void)[] = [];\n const serviceKeys: symbol[] = [];\n try {\n plugin.start({\n client: this.client,\n roomKeyPrefix: this.keyPrefix,\n registerService: (key, service) => {\n if (this.services.has(key.id)) {\n throw new Error(`Backend service \"${key.name}\" is already registered`);\n }\n this.services.set(key.id, service);\n serviceKeys.push(key.id);\n },\n addDisposer: (dispose) => localDisposers.push(dispose),\n });\n } catch (error) {\n for (const dispose of localDisposers.reverse()) safelyDispose(dispose);\n for (const key of serviceKeys) this.services.delete(key);\n throw error;\n }\n this.pluginDisposers.push(() => {\n for (const dispose of localDisposers.reverse()) safelyDispose(dispose);\n for (const key of serviceKeys) this.services.delete(key);\n });\n }\n } catch (error) {\n this.dispose();\n throw error;\n }\n }\n}\n\nfunction safelyDispose(dispose: () => void): void {\n try {\n dispose();\n } catch {\n // Continue rolling back later resources.\n }\n}\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":"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","names":[]}
1
+ {"version":3,"sources":["../src/redis-hub-backend.ts","../src/room-key.ts","../src/redis-hub-fanout.ts"],"sourcesContent":["import type { ServiceKey } from '@fieldnotes/core';\nimport {\n isValidWireElement,\n isValidLayerRecord,\n type LayerRecord,\n type WireSyncElement,\n type WireSyncOp,\n} from '@fieldnotes/sync';\nimport type { HubBackend } from '@fieldnotes/sync-server';\nimport type { RedisHashClient } from './redis-hash-client';\nimport type { BackendSyncPlugin } from './sync-plugin';\nimport { encodeRoomKey } from './room-key';\n\nexport interface RedisHubBackendOptions {\n keyPrefix?: string;\n plugins?: readonly BackendSyncPlugin[];\n}\n\nexport class RedisHubBackend implements HubBackend {\n readonly sharedAcrossInstances = true;\n private readonly client: RedisHashClient;\n private readonly keyPrefix: string;\n private readonly services = new Map<symbol, unknown>();\n private readonly pluginDisposers: (() => void)[] = [];\n\n constructor(client: RedisHashClient, options: RedisHubBackendOptions = {}) {\n this.client = client;\n this.keyPrefix = options.keyPrefix ?? 'fieldnotes:room:';\n this.installPlugins(options.plugins ?? []);\n }\n\n getService<T>(key: ServiceKey<T>): T | undefined {\n return this.services.get(key.id) as T | undefined;\n }\n\n private key(room: string): string {\n return `${this.keyPrefix}${encodeRoomKey(room)}`;\n }\n\n private layersKey(room: string): string {\n return `${this.key(room)}:layers`;\n }\n\n async snapshot(room: string): Promise<WireSyncElement[]> {\n const map = await this.client.hGetAll(this.key(room));\n const out: WireSyncElement[] = [];\n for (const value of Object.values(map)) {\n try {\n const parsed: unknown = JSON.parse(value);\n if (isValidWireElement(parsed)) out.push(parsed);\n } catch {\n // Corrupt fields are isolated from the rest of the room snapshot.\n }\n }\n return out;\n }\n\n async get(room: string, id: string): Promise<WireSyncElement | 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 isValidWireElement(parsed) ? parsed : undefined;\n } catch {\n return undefined;\n }\n }\n\n async apply(room: string, op: WireSyncOp): 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 }\n\n async layerRecords(room: string): Promise<LayerRecord[]> {\n const map = await this.client.hGetAll(this.layersKey(room));\n const out: LayerRecord[] = [];\n for (const value of Object.values(map)) {\n try {\n const parsed: unknown = JSON.parse(value);\n if (isValidLayerRecord(parsed)) out.push(parsed);\n } catch {\n // Corrupt fields are isolated from the rest of the layer ledger.\n }\n }\n return out;\n }\n\n async getLayerRecord(room: string, id: string): Promise<LayerRecord | undefined> {\n const value = await this.client.hGet(this.layersKey(room), id);\n if (value == null) return undefined;\n try {\n const parsed: unknown = JSON.parse(value);\n return isValidLayerRecord(parsed) ? parsed : undefined;\n } catch {\n return undefined;\n }\n }\n\n async applyLayerRecord(room: string, record: LayerRecord): Promise<void> {\n await this.client.hSet(this.layersKey(room), record.id, JSON.stringify(record));\n }\n\n dispose(): void {\n for (const dispose of [...this.pluginDisposers].reverse()) safelyDispose(dispose);\n this.pluginDisposers.length = 0;\n this.services.clear();\n }\n\n private installPlugins(plugins: readonly BackendSyncPlugin[]): void {\n const names = new Set<string>();\n const prefixes = new Set<string>();\n try {\n for (const plugin of plugins) {\n if (names.has(plugin.name))\n throw new Error(`Backend plugin \"${plugin.name}\" is duplicated`);\n if (prefixes.has(plugin.keyPrefix)) {\n throw new Error(`Backend plugin key prefix \"${plugin.keyPrefix}\" is duplicated`);\n }\n names.add(plugin.name);\n prefixes.add(plugin.keyPrefix);\n const localDisposers: (() => void)[] = [];\n const serviceKeys: symbol[] = [];\n try {\n plugin.start({\n client: this.client,\n roomKeyPrefix: this.keyPrefix,\n roomKey: (room) => this.key(room),\n registerService: (key, service) => {\n if (this.services.has(key.id)) {\n throw new Error(`Backend service \"${key.name}\" is already registered`);\n }\n this.services.set(key.id, service);\n serviceKeys.push(key.id);\n },\n addDisposer: (dispose) => localDisposers.push(dispose),\n });\n } catch (error) {\n for (const dispose of localDisposers.reverse()) safelyDispose(dispose);\n for (const key of serviceKeys) this.services.delete(key);\n throw error;\n }\n this.pluginDisposers.push(() => {\n for (const dispose of localDisposers.reverse()) safelyDispose(dispose);\n for (const key of serviceKeys) this.services.delete(key);\n });\n }\n } catch (error) {\n this.dispose();\n throw error;\n }\n }\n}\n\nfunction safelyDispose(dispose: () => void): void {\n try {\n dispose();\n } catch {\n // Continue rolling back later resources.\n }\n}\n","/**\n * Encodes a room name for use inside a Redis key. Room-scoped hashes are laid\n * out as `<prefix><room>` plus `<prefix><room>:<suffix>` sub-keys, so an\n * unescaped `:` in a room name lets `foo:layers` alias room `foo`'s layer\n * ledger. `encodeURIComponent` leaves the relay's valid room alphabet\n * (`[A-Za-z0-9_-]`) untouched, so existing keys keep their historical layout.\n */\nexport function encodeRoomKey(room: string): string {\n return encodeURIComponent(room);\n}\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":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fieldnotes/sync-redis",
3
- "version": "0.8.1",
3
+ "version": "0.10.0",
4
4
  "description": "Redis-backed HubBackend for Field Notes real-time sync relay",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -36,18 +36,18 @@
36
36
  "fieldnotes"
37
37
  ],
38
38
  "dependencies": {
39
- "@fieldnotes/sync": "0.18.1"
39
+ "@fieldnotes/sync": "0.20.0"
40
40
  },
41
41
  "peerDependencies": {
42
- "@fieldnotes/core": ">=0.81.0 <1.0.0"
42
+ "@fieldnotes/core": ">=0.82.0 <1.0.0"
43
43
  },
44
44
  "devDependencies": {
45
45
  "@vitest/coverage-v8": "^4.1.0",
46
46
  "tsup": "^8.5.1",
47
47
  "vitest": "^4.1.0",
48
- "@fieldnotes/sync-server": "0.17.1",
49
- "@fieldnotes/vtt": "0.7.1",
50
- "@fieldnotes/core": "0.81.1"
48
+ "@fieldnotes/vtt": "0.9.0",
49
+ "@fieldnotes/sync-server": "0.19.0",
50
+ "@fieldnotes/core": "0.82.1"
51
51
  },
52
52
  "scripts": {
53
53
  "build": "tsup",