@fieldnotes/sync-redis 0.7.0 → 0.7.1

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
@@ -131,7 +131,7 @@ var RedisHubBackend = class {
131
131
  }
132
132
  if (!(0, import_sync.isValidFogTileRecord)(parsed)) continue;
133
133
  if (parsed.generation !== definition.generation) continue;
134
- if (!tileIntersectsDefinition(parsed.x, parsed.y, definition)) continue;
134
+ if (!(0, import_vtt.tileIntersectsDefinition)(parsed.x, parsed.y, definition)) continue;
135
135
  if (!(0, import_sync.isValidFogSnapshot)({ meta, tiles: [parsed] })) continue;
136
136
  const key = `${parsed.x},${parsed.y}`;
137
137
  if (seen.has(key)) continue;
@@ -167,7 +167,7 @@ var RedisHubBackend = class {
167
167
  }
168
168
  if (!(0, import_sync.isValidFogSnapshot)({ meta: current, tiles: [tile] })) continue;
169
169
  const valid = tile;
170
- if (!tileIntersectsDefinition(valid.x, valid.y, record.definition)) continue;
170
+ if (!(0, import_vtt.tileIntersectsDefinition)(valid.x, valid.y, record.definition)) continue;
171
171
  if (valid.data === void 0) {
172
172
  replacements.push(valid);
173
173
  continue;
@@ -179,13 +179,13 @@ var RedisHubBackend = class {
179
179
  if (canonical) replacements.push({ ...valid, data: canonical.data });
180
180
  }
181
181
  }
182
- const result = await this.evalFog(FOG_META_LWW_SCRIPT, room, record, [
182
+ const result = await this.evalFog(import_vtt.FOG_META_LWW_SCRIPT, room, record, [
183
183
  expectedMeta ?? "",
184
184
  JSON.stringify(expectedTiles),
185
185
  JSON.stringify(replacements)
186
186
  ]);
187
187
  if (Array.isArray(result) && result[0] === 2) continue;
188
- return parseApplyResult(result, import_sync.isValidFogMetaRecord);
188
+ return (0, import_vtt.parseFogRedisMetaResult)(result, import_sync.isValidFogMetaRecord);
189
189
  }
190
190
  throw new Error("Redis fog meta update did not converge after concurrent writes");
191
191
  }
@@ -242,12 +242,12 @@ var RedisHubBackend = class {
242
242
  })
243
243
  };
244
244
  }
245
- const result = await this.evalFog(FOG_PATCH_LWW_SCRIPT, room, records, [
245
+ const result = await this.evalFog(import_vtt.FOG_PATCH_LWW_SCRIPT, room, records, [
246
246
  metaRaw,
247
247
  JSON.stringify(invalidStored)
248
248
  ]);
249
249
  if (Array.isArray(result) && result[0] === 2) continue;
250
- return parsePatchApplyResult(result);
250
+ return (0, import_vtt.parseFogRedisPatchResult)(result);
251
251
  }
252
252
  throw new Error("Redis fog patch did not converge after concurrent definition writes");
253
253
  }
@@ -261,229 +261,6 @@ var RedisHubBackend = class {
261
261
  });
262
262
  }
263
263
  };
264
- function tileIntersectsDefinition(x, y, definition) {
265
- const tileWorldSize = 128 * definition.cellSize;
266
- const tileWorldX = x * tileWorldSize;
267
- const tileWorldY = y * tileWorldSize;
268
- return !(tileWorldX + tileWorldSize <= definition.bounds.x || tileWorldY + tileWorldSize <= definition.bounds.y || tileWorldX >= definition.bounds.x + definition.bounds.w || tileWorldY >= definition.bounds.y + definition.bounds.h);
269
- }
270
- function parseApplyResult(raw, guard) {
271
- if (!Array.isArray(raw) || raw[0] !== 0 && raw[0] !== 1) {
272
- throw new Error("Redis returned an invalid fog apply result");
273
- }
274
- if (raw[0] === 1) return { accepted: true };
275
- if (typeof raw[1] !== "string" || raw[1].length === 0) return { accepted: false };
276
- let correction;
277
- try {
278
- correction = JSON.parse(raw[1]);
279
- } catch {
280
- throw new Error("Redis returned an invalid fog correction");
281
- }
282
- if (!guard(correction)) throw new Error("Redis returned an invalid fog correction");
283
- return { accepted: false, correction };
284
- }
285
- function parsePatchApplyResult(raw) {
286
- if (!Array.isArray(raw) || typeof raw[0] !== "number") {
287
- throw new Error("Redis returned an invalid fog patch result");
288
- }
289
- const acceptedCount = raw[0];
290
- if (!Number.isSafeInteger(acceptedCount) || acceptedCount < 0) {
291
- throw new Error("Redis returned an invalid fog patch result");
292
- }
293
- const accepted = [];
294
- let cursor = 1;
295
- for (let i = 0; i < acceptedCount; i++, cursor++) {
296
- const parsed = parseFogTileResultRecord(raw[cursor]);
297
- accepted.push(parsed);
298
- }
299
- const correctionCount = raw[cursor++];
300
- if (!Number.isSafeInteger(correctionCount) || correctionCount < 0) {
301
- throw new Error("Redis returned an invalid fog patch result");
302
- }
303
- const corrections = [];
304
- for (let i = 0; i < correctionCount; i++, cursor++) {
305
- corrections.push(parseFogTileResultRecord(raw[cursor]));
306
- }
307
- if (cursor !== raw.length) throw new Error("Redis returned an invalid fog patch result");
308
- return { accepted, corrections };
309
- }
310
- function parseFogTileResultRecord(raw) {
311
- if (typeof raw !== "string") throw new Error("Redis returned an invalid fog patch record");
312
- let parsed;
313
- try {
314
- parsed = JSON.parse(raw);
315
- } catch {
316
- throw new Error("Redis returned an invalid fog patch record");
317
- }
318
- if (!(0, import_sync.isValidFogTileRecord)(parsed)) throw new Error("Redis returned an invalid fog patch record");
319
- return parsed;
320
- }
321
- var FOG_META_LWW_SCRIPT = `
322
- local incomingRaw = ARGV[1]
323
- local incoming = cjson.decode(incomingRaw)
324
- local currentRaw = redis.call('HGET', KEYS[1], 'current')
325
- local expectedMetaRaw = ARGV[2]
326
- if (currentRaw or '') ~= expectedMetaRaw then return {2} end
327
- local expectedTiles = cjson.decode(ARGV[3])
328
- local expectedTileCount = 0
329
- for field, raw in pairs(expectedTiles) do
330
- expectedTileCount = expectedTileCount + 1
331
- if redis.call('HGET', KEYS[2], field) ~= raw then return {2} end
332
- end
333
- if redis.call('HLEN', KEYS[2]) ~= expectedTileCount then return {2} end
334
- local replacementTiles = cjson.decode(ARGV[4])
335
- local function ascii(value)
336
- if type(value) ~= 'string' or #value < 1 or #value > 128 then return false end
337
- for i = 1, #value do
338
- local b = string.byte(value, i)
339
- if b < 32 or b > 126 then return false end
340
- end
341
- return true
342
- end
343
- local function integer(value)
344
- return type(value) == 'number' and value >= 1 and value <= 9007199254740991
345
- and value == math.floor(value)
346
- end
347
- local function validDef(def)
348
- return type(def) == 'table' and def.version == 1 and ascii(def.generation)
349
- and type(def.bounds) == 'table' and type(def.bounds.x) == 'number'
350
- and type(def.bounds.y) == 'number' and type(def.bounds.w) == 'number' and def.bounds.w > 0
351
- and type(def.bounds.h) == 'number' and def.bounds.h > 0 and type(def.cellSize) == 'number'
352
- and def.cellSize > 0 and def.tileCells == 128 and (def.base == 'covered' or def.base == 'revealed')
353
- end
354
- local function validMeta(value)
355
- return type(value) == 'table' and integer(value.version) and ascii(value.editor)
356
- and (value.definition == nil or validDef(value.definition))
357
- end
358
- local current = nil
359
- if currentRaw then
360
- local ok, decoded = pcall(cjson.decode, currentRaw)
361
- if ok and validMeta(decoded) then current = decoded else redis.call('HDEL', KEYS[1], 'current') end
362
- end
363
- local function newer(a, b)
364
- return a.version > b.version or (a.version == b.version and a.editor > b.editor)
365
- end
366
- if current and not newer(incoming, current) then return {0, currentRaw} end
367
- local oldDef = current and current.definition or nil
368
- local newDef = incoming.definition
369
- if oldDef and newDef and oldDef.generation == newDef.generation
370
- and (oldDef.cellSize ~= newDef.cellSize or oldDef.tileCells ~= newDef.tileCells
371
- or oldDef.base ~= newDef.base or newDef.bounds.x > oldDef.bounds.x
372
- or newDef.bounds.y > oldDef.bounds.y
373
- or newDef.bounds.x + newDef.bounds.w < oldDef.bounds.x + oldDef.bounds.w
374
- or newDef.bounds.y + newDef.bounds.h < oldDef.bounds.y + oldDef.bounds.h) then
375
- return {0, currentRaw}
376
- end
377
-
378
- redis.call('HSET', KEYS[1], 'current', incomingRaw)
379
- redis.call('DEL', KEYS[2])
380
- if newDef and oldDef and oldDef.generation == newDef.generation then
381
- for i = 1, #replacementTiles do
382
- local tile = replacementTiles[i]
383
- redis.call('HSET', KEYS[2], tostring(tile.x) .. ',' .. tostring(tile.y), cjson.encode(tile))
384
- end
385
- end
386
- return {1}
387
- `;
388
- var FOG_PATCH_LWW_SCRIPT = `
389
- local incomingRaws = cjson.decode(ARGV[1])
390
- local metaRaw = redis.call('HGET', KEYS[1], 'current')
391
- if (metaRaw or '') ~= ARGV[2] then return {2} end
392
- local invalidStored = cjson.decode(ARGV[3])
393
- for field, raw in pairs(invalidStored) do
394
- if redis.call('HGET', KEYS[2], field) == raw then redis.call('HDEL', KEYS[2], field) end
395
- end
396
- if not metaRaw then return {0, 0} end
397
- local metaOk, meta = pcall(cjson.decode, metaRaw)
398
- if not metaOk or type(meta) ~= 'table' or type(meta.definition) ~= 'table'
399
- or type(meta.definition.generation) ~= 'string' then return {0, 0} end
400
- local def = meta.definition
401
- local tileSize = 128 * def.cellSize
402
- local function intersects(tile)
403
- local tx = tile.x * tileSize
404
- local ty = tile.y * tileSize
405
- return tx + tileSize > def.bounds.x and ty + tileSize > def.bounds.y
406
- and tx < def.bounds.x + def.bounds.w and ty < def.bounds.y + def.bounds.h
407
- end
408
- local function newer(a, b)
409
- return a.version > b.version or (a.version == b.version and a.editor > b.editor)
410
- end
411
- local function validTile(tile)
412
- if type(tile) ~= 'table' then return false end
413
- local function ascii(value)
414
- if type(value) ~= 'string' or #value < 1 or #value > 128 then return false end
415
- for i = 1, #value do
416
- local b = string.byte(value, i)
417
- if b < 32 or b > 126 then return false end
418
- end
419
- return true
420
- end
421
- local dataOk = tile.data == nil or (type(tile.data) == 'string' and #tile.data == 2732
422
- and string.match(tile.data, '^[A-Za-z0-9+/]+[AEIMQUYcgkosw048]=$') ~= nil)
423
- return ascii(tile.generation)
424
- and type(tile.x) == 'number' and math.abs(tile.x) <= 9007199254740991
425
- and tile.x == math.floor(tile.x)
426
- and type(tile.y) == 'number' and math.abs(tile.y) <= 9007199254740991
427
- and tile.y == math.floor(tile.y)
428
- and type(tile.version) == 'number' and tile.version >= 1
429
- and tile.version <= 9007199254740991 and tile.version == math.floor(tile.version)
430
- and ascii(tile.editor) and dataOk
431
- end
432
- local stored = redis.call('HGETALL', KEYS[2])
433
- local validCount = 0
434
- for i = 1, #stored, 2 do
435
- local ok, tile = pcall(cjson.decode, stored[i + 1])
436
- if not ok or not validTile(tile) or stored[i] ~= tostring(tile.x) .. ',' .. tostring(tile.y)
437
- or tile.generation ~= def.generation or not intersects(tile) then
438
- redis.call('HDEL', KEYS[2], stored[i])
439
- else
440
- validCount = validCount + 1
441
- end
442
- end
443
- local accepted = {}
444
- local corrections = {}
445
- local newCount = 0
446
- local currents = {}
447
- for i = 1, #incomingRaws do
448
- local incomingRaw = cjson.encode(incomingRaws[i])
449
- local incoming = incomingRaws[i]
450
- local field = tostring(incoming.x) .. ',' .. tostring(incoming.y)
451
- local currentRaw = redis.call('HGET', KEYS[2], field)
452
- local current = nil
453
- if currentRaw then
454
- local ok, decoded = pcall(cjson.decode, currentRaw)
455
- if ok and validTile(decoded) then current = decoded else currentRaw = nil end
456
- end
457
- currents[i] = currentRaw or false
458
- if incoming.generation ~= def.generation or not intersects(incoming)
459
- or (current and not newer(incoming, current)) then
460
- corrections[#corrections + 1] = currentRaw or cjson.encode({generation=def.generation,
461
- x=incoming.x, y=incoming.y, version=1, editor='hub'})
462
- else
463
- accepted[#accepted + 1] = incomingRaw
464
- if not current then newCount = newCount + 1 end
465
- end
466
- end
467
- if validCount + newCount > 256 then
468
- accepted = {}
469
- corrections = {}
470
- for i = 1, #incomingRaws do
471
- local incoming = incomingRaws[i]
472
- corrections[#corrections + 1] = currents[i] or cjson.encode({generation=def.generation,
473
- x=incoming.x, y=incoming.y, version=1, editor='hub'})
474
- end
475
- else
476
- for i = 1, #accepted do
477
- local record = cjson.decode(accepted[i])
478
- redis.call('HSET', KEYS[2], tostring(record.x) .. ',' .. tostring(record.y), accepted[i])
479
- end
480
- end
481
- local result = {#accepted}
482
- for i = 1, #accepted do result[#result + 1] = accepted[i] end
483
- result[#result + 1] = #corrections
484
- for i = 1, #corrections do result[#result + 1] = corrections[i] end
485
- return result
486
- `;
487
264
 
488
265
  // src/redis-hub-fanout.ts
489
266
  var RedisHubFanout = class {
@@ -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 {\n BackendSyncPlugin,\n ApplyResult,\n BackendOpContext,\n PluginSnapshot,\n} from './sync-plugin';\n","import {\n isValidElement,\n isValidLayerRecord,\n isValidFogMetaRecord,\n isValidFogTileRecord,\n isValidFogSnapshot,\n type LayerRecord,\n type SyncOp,\n type FogSnapshot,\n type FogMetaRecord,\n type FogTileRecord,\n} from '@fieldnotes/sync';\nimport { canonicalizeFogTile } from '@fieldnotes/vtt';\nimport type { CanvasElement } from '@fieldnotes/core';\nimport type { FogApplyResult, FogPatchApplyResult, 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 readonly sharedAcrossInstances = true;\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 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 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 // 'clear' deletes elements only; the layer ledger is a separate hash and survives.\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 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 let parsed: unknown;\n try {\n parsed = JSON.parse(value);\n } catch {\n continue; // skip a corrupt stored value rather than throwing the whole ledger\n }\n if (isValidLayerRecord(parsed)) out.push(parsed);\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 private fogMetaKey(room: string): string {\n return `${this.keyPrefix}${room}:fog:meta`;\n }\n\n private fogTilesKey(room: string): string {\n return `${this.keyPrefix}${room}:fog:tiles`;\n }\n\n async fogSnapshot(room: string): Promise<FogSnapshot | undefined> {\n const metaStr = await this.client.hGet(this.fogMetaKey(room), 'current');\n if (metaStr == null) return undefined;\n let meta: unknown;\n try {\n meta = JSON.parse(metaStr);\n } catch {\n return undefined;\n }\n if (!isValidFogMetaRecord(meta)) return undefined;\n\n if (!(meta as FogMetaRecord).definition) return { meta: meta as FogMetaRecord, tiles: [] };\n\n const definition = (meta as FogMetaRecord).definition;\n if (!definition) return { meta: meta as FogMetaRecord, tiles: [] };\n const tileMap = await this.client.hGetAll(this.fogTilesKey(room));\n const tiles: FogTileRecord[] = [];\n const seen = new Set<string>();\n for (const value of Object.values(tileMap)) {\n let parsed: unknown;\n try {\n parsed = JSON.parse(value);\n } catch {\n continue;\n }\n if (!isValidFogTileRecord(parsed)) continue;\n if (parsed.generation !== definition.generation) continue;\n if (!tileIntersectsDefinition(parsed.x, parsed.y, definition)) continue;\n if (!isValidFogSnapshot({ meta: meta as FogMetaRecord, tiles: [parsed] })) continue;\n const key = `${parsed.x},${parsed.y}`;\n if (seen.has(key)) continue;\n seen.add(key);\n tiles.push(parsed);\n if (tiles.length === 256) break;\n }\n\n const snapshot = { meta: meta as FogMetaRecord, tiles };\n return isValidFogSnapshot(snapshot) ? snapshot : undefined;\n }\n\n async applyFogMeta(room: string, record: FogMetaRecord): Promise<FogApplyResult<FogMetaRecord>> {\n for (let attempt = 0; attempt < 4; attempt++) {\n const metaKey = this.fogMetaKey(room);\n const tilesKey = this.fogTilesKey(room);\n const expectedMeta = await this.client.hGet(metaKey, 'current');\n const expectedTiles = await this.client.hGetAll(tilesKey);\n const replacements: FogTileRecord[] = [];\n let current: FogMetaRecord | undefined;\n if (expectedMeta !== null) {\n try {\n const parsed: unknown = JSON.parse(expectedMeta);\n if (isValidFogMetaRecord(parsed)) current = parsed;\n } catch {\n // Corrupt state is atomically replaced by a valid winning record.\n }\n }\n if (\n current?.definition &&\n record.definition &&\n current.definition.generation === record.definition.generation\n ) {\n for (const raw of Object.values(expectedTiles)) {\n let tile: unknown;\n try {\n tile = JSON.parse(raw);\n } catch {\n continue;\n }\n if (!isValidFogSnapshot({ meta: current, tiles: [tile] })) continue;\n const valid = tile as FogTileRecord;\n if (!tileIntersectsDefinition(valid.x, valid.y, record.definition)) continue;\n if (valid.data === undefined) {\n replacements.push(valid);\n continue;\n }\n const canonical = canonicalizeFogTile(\n { x: valid.x, y: valid.y, data: valid.data },\n record.definition,\n );\n if (canonical) replacements.push({ ...valid, data: canonical.data });\n }\n }\n const result = await this.evalFog(FOG_META_LWW_SCRIPT, room, record, [\n expectedMeta ?? '',\n JSON.stringify(expectedTiles),\n JSON.stringify(replacements),\n ]);\n if (Array.isArray(result) && result[0] === 2) continue;\n return parseApplyResult(result, isValidFogMetaRecord);\n }\n throw new Error('Redis fog meta update did not converge after concurrent writes');\n }\n\n async applyFogTile(room: string, record: FogTileRecord): Promise<FogApplyResult<FogTileRecord>> {\n const result = await this.applyFogPatch(room, [record]);\n if (result.accepted.length === 1) return { accepted: true };\n return { accepted: false, correction: result.corrections[0] };\n }\n\n async applyFogPatch(\n room: string,\n records: readonly FogTileRecord[],\n ): Promise<FogPatchApplyResult> {\n for (let attempt = 0; attempt < 4; attempt++) {\n const metaRaw = await this.client.hGet(this.fogMetaKey(room), 'current');\n if (metaRaw === null) return { accepted: [], corrections: [] };\n let meta: unknown;\n try {\n meta = JSON.parse(metaRaw);\n } catch {\n return { accepted: [], corrections: [] };\n }\n if (!isValidFogMetaRecord(meta) || !meta.definition) {\n return { accepted: [], corrections: [] };\n }\n const definition = meta.definition;\n const stored = await this.client.hGetAll(this.fogTilesKey(room));\n const invalidStored: Record<string, string> = {};\n for (const [field, raw] of Object.entries(stored)) {\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n invalidStored[field] = raw;\n continue;\n }\n if (!isValidFogSnapshot({ meta, tiles: [parsed] })) invalidStored[field] = raw;\n }\n if (records.some((tile) => !isValidFogSnapshot({ meta, tiles: [tile] }))) {\n return {\n accepted: [],\n corrections: records.map((tile) => {\n const raw = stored[`${tile.x},${tile.y}`];\n if (raw) {\n try {\n const parsed: unknown = JSON.parse(raw);\n if (isValidFogSnapshot({ meta, tiles: [parsed] })) return parsed as FogTileRecord;\n } catch {\n // Fall through to an authoritative tombstone.\n }\n }\n return {\n generation: definition.generation,\n x: tile.x,\n y: tile.y,\n version: 1,\n editor: 'hub',\n };\n }),\n };\n }\n const result = await this.evalFog(FOG_PATCH_LWW_SCRIPT, room, records, [\n metaRaw,\n JSON.stringify(invalidStored),\n ]);\n if (Array.isArray(result) && result[0] === 2) continue;\n return parsePatchApplyResult(result);\n }\n throw new Error('Redis fog patch did not converge after concurrent definition writes');\n }\n\n private async evalFog(\n script: string,\n room: string,\n record: object,\n extraArguments: string[] = [],\n ): Promise<unknown> {\n if (!this.client.eval) {\n throw new Error('Redis fog persistence requires a Redis client with EVAL support');\n }\n return this.client.eval(script, {\n keys: [this.fogMetaKey(room), this.fogTilesKey(room)],\n arguments: [JSON.stringify(record), ...extraArguments],\n });\n }\n}\n\nfunction tileIntersectsDefinition(\n x: number,\n y: number,\n definition: NonNullable<FogMetaRecord['definition']>,\n): boolean {\n const tileWorldSize = 128 * definition.cellSize;\n const tileWorldX = x * tileWorldSize;\n const tileWorldY = y * tileWorldSize;\n return !(\n tileWorldX + tileWorldSize <= definition.bounds.x ||\n tileWorldY + tileWorldSize <= definition.bounds.y ||\n tileWorldX >= definition.bounds.x + definition.bounds.w ||\n tileWorldY >= definition.bounds.y + definition.bounds.h\n );\n}\n\nfunction parseApplyResult<T>(\n raw: unknown,\n guard: (value: unknown) => value is T,\n): FogApplyResult<T> {\n if (!Array.isArray(raw) || (raw[0] !== 0 && raw[0] !== 1)) {\n throw new Error('Redis returned an invalid fog apply result');\n }\n if (raw[0] === 1) return { accepted: true };\n if (typeof raw[1] !== 'string' || raw[1].length === 0) return { accepted: false };\n let correction: unknown;\n try {\n correction = JSON.parse(raw[1]);\n } catch {\n throw new Error('Redis returned an invalid fog correction');\n }\n if (!guard(correction)) throw new Error('Redis returned an invalid fog correction');\n return { accepted: false, correction };\n}\n\nfunction parsePatchApplyResult(raw: unknown): FogPatchApplyResult {\n if (!Array.isArray(raw) || typeof raw[0] !== 'number') {\n throw new Error('Redis returned an invalid fog patch result');\n }\n const acceptedCount = raw[0];\n if (!Number.isSafeInteger(acceptedCount) || acceptedCount < 0) {\n throw new Error('Redis returned an invalid fog patch result');\n }\n const accepted: FogTileRecord[] = [];\n let cursor = 1;\n for (let i = 0; i < acceptedCount; i++, cursor++) {\n const parsed = parseFogTileResultRecord(raw[cursor]);\n accepted.push(parsed);\n }\n const correctionCount = raw[cursor++];\n if (!Number.isSafeInteger(correctionCount) || (correctionCount as number) < 0) {\n throw new Error('Redis returned an invalid fog patch result');\n }\n const corrections: FogTileRecord[] = [];\n for (let i = 0; i < (correctionCount as number); i++, cursor++) {\n corrections.push(parseFogTileResultRecord(raw[cursor]));\n }\n if (cursor !== raw.length) throw new Error('Redis returned an invalid fog patch result');\n return { accepted, corrections };\n}\n\nfunction parseFogTileResultRecord(raw: unknown): FogTileRecord {\n if (typeof raw !== 'string') throw new Error('Redis returned an invalid fog patch record');\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n throw new Error('Redis returned an invalid fog patch record');\n }\n if (!isValidFogTileRecord(parsed)) throw new Error('Redis returned an invalid fog patch record');\n return parsed;\n}\n\nconst FOG_META_LWW_SCRIPT = `\nlocal incomingRaw = ARGV[1]\nlocal incoming = cjson.decode(incomingRaw)\nlocal currentRaw = redis.call('HGET', KEYS[1], 'current')\nlocal expectedMetaRaw = ARGV[2]\nif (currentRaw or '') ~= expectedMetaRaw then return {2} end\nlocal expectedTiles = cjson.decode(ARGV[3])\nlocal expectedTileCount = 0\nfor field, raw in pairs(expectedTiles) do\n expectedTileCount = expectedTileCount + 1\n if redis.call('HGET', KEYS[2], field) ~= raw then return {2} end\nend\nif redis.call('HLEN', KEYS[2]) ~= expectedTileCount then return {2} end\nlocal replacementTiles = cjson.decode(ARGV[4])\nlocal function ascii(value)\n if type(value) ~= 'string' or #value < 1 or #value > 128 then return false end\n for i = 1, #value do\n local b = string.byte(value, i)\n if b < 32 or b > 126 then return false end\n end\n return true\nend\nlocal function integer(value)\n return type(value) == 'number' and value >= 1 and value <= 9007199254740991\n and value == math.floor(value)\nend\nlocal function validDef(def)\n return type(def) == 'table' and def.version == 1 and ascii(def.generation)\n and type(def.bounds) == 'table' and type(def.bounds.x) == 'number'\n and type(def.bounds.y) == 'number' and type(def.bounds.w) == 'number' and def.bounds.w > 0\n and type(def.bounds.h) == 'number' and def.bounds.h > 0 and type(def.cellSize) == 'number'\n and def.cellSize > 0 and def.tileCells == 128 and (def.base == 'covered' or def.base == 'revealed')\nend\nlocal function validMeta(value)\n return type(value) == 'table' and integer(value.version) and ascii(value.editor)\n and (value.definition == nil or validDef(value.definition))\nend\nlocal current = nil\nif currentRaw then\n local ok, decoded = pcall(cjson.decode, currentRaw)\n if ok and validMeta(decoded) then current = decoded else redis.call('HDEL', KEYS[1], 'current') end\nend\nlocal function newer(a, b)\n return a.version > b.version or (a.version == b.version and a.editor > b.editor)\nend\nif current and not newer(incoming, current) then return {0, currentRaw} end\nlocal oldDef = current and current.definition or nil\nlocal newDef = incoming.definition\nif oldDef and newDef and oldDef.generation == newDef.generation\n and (oldDef.cellSize ~= newDef.cellSize or oldDef.tileCells ~= newDef.tileCells\n or oldDef.base ~= newDef.base or newDef.bounds.x > oldDef.bounds.x\n or newDef.bounds.y > oldDef.bounds.y\n or newDef.bounds.x + newDef.bounds.w < oldDef.bounds.x + oldDef.bounds.w\n or newDef.bounds.y + newDef.bounds.h < oldDef.bounds.y + oldDef.bounds.h) then\n return {0, currentRaw}\nend\n\nredis.call('HSET', KEYS[1], 'current', incomingRaw)\nredis.call('DEL', KEYS[2])\nif newDef and oldDef and oldDef.generation == newDef.generation then\n for i = 1, #replacementTiles do\n local tile = replacementTiles[i]\n redis.call('HSET', KEYS[2], tostring(tile.x) .. ',' .. tostring(tile.y), cjson.encode(tile))\n end\nend\nreturn {1}\n`;\n\nconst FOG_PATCH_LWW_SCRIPT = `\nlocal incomingRaws = cjson.decode(ARGV[1])\nlocal metaRaw = redis.call('HGET', KEYS[1], 'current')\nif (metaRaw or '') ~= ARGV[2] then return {2} end\nlocal invalidStored = cjson.decode(ARGV[3])\nfor field, raw in pairs(invalidStored) do\n if redis.call('HGET', KEYS[2], field) == raw then redis.call('HDEL', KEYS[2], field) end\nend\nif not metaRaw then return {0, 0} end\nlocal metaOk, meta = pcall(cjson.decode, metaRaw)\nif not metaOk or type(meta) ~= 'table' or type(meta.definition) ~= 'table'\n or type(meta.definition.generation) ~= 'string' then return {0, 0} end\nlocal def = meta.definition\nlocal tileSize = 128 * def.cellSize\nlocal function intersects(tile)\n local tx = tile.x * tileSize\n local ty = tile.y * tileSize\n return tx + tileSize > def.bounds.x and ty + tileSize > def.bounds.y\n and tx < def.bounds.x + def.bounds.w and ty < def.bounds.y + def.bounds.h\nend\nlocal function newer(a, b)\n return a.version > b.version or (a.version == b.version and a.editor > b.editor)\nend\nlocal function validTile(tile)\n if type(tile) ~= 'table' then return false end\n local function ascii(value)\n if type(value) ~= 'string' or #value < 1 or #value > 128 then return false end\n for i = 1, #value do\n local b = string.byte(value, i)\n if b < 32 or b > 126 then return false end\n end\n return true\n end\n local dataOk = tile.data == nil or (type(tile.data) == 'string' and #tile.data == 2732\n and string.match(tile.data, '^[A-Za-z0-9+/]+[AEIMQUYcgkosw048]=$') ~= nil)\n return ascii(tile.generation)\n and type(tile.x) == 'number' and math.abs(tile.x) <= 9007199254740991\n and tile.x == math.floor(tile.x)\n and type(tile.y) == 'number' and math.abs(tile.y) <= 9007199254740991\n and tile.y == math.floor(tile.y)\n and type(tile.version) == 'number' and tile.version >= 1\n and tile.version <= 9007199254740991 and tile.version == math.floor(tile.version)\n and ascii(tile.editor) and dataOk\nend\nlocal stored = redis.call('HGETALL', KEYS[2])\nlocal validCount = 0\nfor i = 1, #stored, 2 do\n local ok, tile = pcall(cjson.decode, stored[i + 1])\n if not ok or not validTile(tile) or stored[i] ~= tostring(tile.x) .. ',' .. tostring(tile.y)\n or tile.generation ~= def.generation or not intersects(tile) then\n redis.call('HDEL', KEYS[2], stored[i])\n else\n validCount = validCount + 1\n end\nend\nlocal accepted = {}\nlocal corrections = {}\nlocal newCount = 0\nlocal currents = {}\nfor i = 1, #incomingRaws do\n local incomingRaw = cjson.encode(incomingRaws[i])\n local incoming = incomingRaws[i]\n local field = tostring(incoming.x) .. ',' .. tostring(incoming.y)\n local currentRaw = redis.call('HGET', KEYS[2], field)\n local current = nil\n if currentRaw then\n local ok, decoded = pcall(cjson.decode, currentRaw)\n if ok and validTile(decoded) then current = decoded else currentRaw = nil end\n end\n currents[i] = currentRaw or false\n if incoming.generation ~= def.generation or not intersects(incoming)\n or (current and not newer(incoming, current)) then\n corrections[#corrections + 1] = currentRaw or cjson.encode({generation=def.generation,\n x=incoming.x, y=incoming.y, version=1, editor='hub'})\n else\n accepted[#accepted + 1] = incomingRaw\n if not current then newCount = newCount + 1 end\n end\nend\nif validCount + newCount > 256 then\n accepted = {}\n corrections = {}\n for i = 1, #incomingRaws do\n local incoming = incomingRaws[i]\n corrections[#corrections + 1] = currents[i] or cjson.encode({generation=def.generation,\n x=incoming.x, y=incoming.y, version=1, editor='hub'})\n end\nelse\n for i = 1, #accepted do\n local record = cjson.decode(accepted[i])\n redis.call('HSET', KEYS[2], tostring(record.x) .. ',' .. tostring(record.y), accepted[i])\n end\nend\nlocal result = {#accepted}\nfor i = 1, #accepted do result[#result + 1] = accepted[i] end\nresult[#result + 1] = #corrections\nfor i = 1, #corrections do result[#result + 1] = corrections[i] end\nreturn result\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/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 {\n BackendSyncPlugin,\n ApplyResult,\n BackendOpContext,\n PluginSnapshot,\n} from './sync-plugin';\n","import {\n isValidElement,\n isValidLayerRecord,\n isValidFogMetaRecord,\n isValidFogTileRecord,\n isValidFogSnapshot,\n type LayerRecord,\n type SyncOp,\n type FogSnapshot,\n type FogMetaRecord,\n type FogTileRecord,\n} from '@fieldnotes/sync';\nimport {\n canonicalizeFogTile,\n tileIntersectsDefinition,\n parseFogRedisMetaResult,\n parseFogRedisPatchResult,\n FOG_META_LWW_SCRIPT,\n FOG_PATCH_LWW_SCRIPT,\n} from '@fieldnotes/vtt';\nimport type { CanvasElement } from '@fieldnotes/core';\nimport type { FogApplyResult, FogPatchApplyResult, 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 readonly sharedAcrossInstances = true;\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 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 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 // 'clear' deletes elements only; the layer ledger is a separate hash and survives.\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 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 let parsed: unknown;\n try {\n parsed = JSON.parse(value);\n } catch {\n continue; // skip a corrupt stored value rather than throwing the whole ledger\n }\n if (isValidLayerRecord(parsed)) out.push(parsed);\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 private fogMetaKey(room: string): string {\n return `${this.keyPrefix}${room}:fog:meta`;\n }\n\n private fogTilesKey(room: string): string {\n return `${this.keyPrefix}${room}:fog:tiles`;\n }\n\n async fogSnapshot(room: string): Promise<FogSnapshot | undefined> {\n const metaStr = await this.client.hGet(this.fogMetaKey(room), 'current');\n if (metaStr == null) return undefined;\n let meta: unknown;\n try {\n meta = JSON.parse(metaStr);\n } catch {\n return undefined;\n }\n if (!isValidFogMetaRecord(meta)) return undefined;\n\n if (!(meta as FogMetaRecord).definition) return { meta: meta as FogMetaRecord, tiles: [] };\n\n const definition = (meta as FogMetaRecord).definition;\n if (!definition) return { meta: meta as FogMetaRecord, tiles: [] };\n const tileMap = await this.client.hGetAll(this.fogTilesKey(room));\n const tiles: FogTileRecord[] = [];\n const seen = new Set<string>();\n for (const value of Object.values(tileMap)) {\n let parsed: unknown;\n try {\n parsed = JSON.parse(value);\n } catch {\n continue;\n }\n if (!isValidFogTileRecord(parsed)) continue;\n if (parsed.generation !== definition.generation) continue;\n if (!tileIntersectsDefinition(parsed.x, parsed.y, definition)) continue;\n if (!isValidFogSnapshot({ meta: meta as FogMetaRecord, tiles: [parsed] })) continue;\n const key = `${parsed.x},${parsed.y}`;\n if (seen.has(key)) continue;\n seen.add(key);\n tiles.push(parsed);\n if (tiles.length === 256) break;\n }\n\n const snapshot = { meta: meta as FogMetaRecord, tiles };\n return isValidFogSnapshot(snapshot) ? snapshot : undefined;\n }\n\n async applyFogMeta(room: string, record: FogMetaRecord): Promise<FogApplyResult<FogMetaRecord>> {\n for (let attempt = 0; attempt < 4; attempt++) {\n const metaKey = this.fogMetaKey(room);\n const tilesKey = this.fogTilesKey(room);\n const expectedMeta = await this.client.hGet(metaKey, 'current');\n const expectedTiles = await this.client.hGetAll(tilesKey);\n const replacements: FogTileRecord[] = [];\n let current: FogMetaRecord | undefined;\n if (expectedMeta !== null) {\n try {\n const parsed: unknown = JSON.parse(expectedMeta);\n if (isValidFogMetaRecord(parsed)) current = parsed;\n } catch {\n // Corrupt state is atomically replaced by a valid winning record.\n }\n }\n if (\n current?.definition &&\n record.definition &&\n current.definition.generation === record.definition.generation\n ) {\n for (const raw of Object.values(expectedTiles)) {\n let tile: unknown;\n try {\n tile = JSON.parse(raw);\n } catch {\n continue;\n }\n if (!isValidFogSnapshot({ meta: current, tiles: [tile] })) continue;\n const valid = tile as FogTileRecord;\n if (!tileIntersectsDefinition(valid.x, valid.y, record.definition)) continue;\n if (valid.data === undefined) {\n replacements.push(valid);\n continue;\n }\n const canonical = canonicalizeFogTile(\n { x: valid.x, y: valid.y, data: valid.data },\n record.definition,\n );\n if (canonical) replacements.push({ ...valid, data: canonical.data });\n }\n }\n const result = await this.evalFog(FOG_META_LWW_SCRIPT, room, record, [\n expectedMeta ?? '',\n JSON.stringify(expectedTiles),\n JSON.stringify(replacements),\n ]);\n if (Array.isArray(result) && result[0] === 2) continue;\n return parseFogRedisMetaResult(result, isValidFogMetaRecord);\n }\n throw new Error('Redis fog meta update did not converge after concurrent writes');\n }\n\n async applyFogTile(room: string, record: FogTileRecord): Promise<FogApplyResult<FogTileRecord>> {\n const result = await this.applyFogPatch(room, [record]);\n if (result.accepted.length === 1) return { accepted: true };\n return { accepted: false, correction: result.corrections[0] };\n }\n\n async applyFogPatch(\n room: string,\n records: readonly FogTileRecord[],\n ): Promise<FogPatchApplyResult> {\n for (let attempt = 0; attempt < 4; attempt++) {\n const metaRaw = await this.client.hGet(this.fogMetaKey(room), 'current');\n if (metaRaw === null) return { accepted: [], corrections: [] };\n let meta: unknown;\n try {\n meta = JSON.parse(metaRaw);\n } catch {\n return { accepted: [], corrections: [] };\n }\n if (!isValidFogMetaRecord(meta) || !meta.definition) {\n return { accepted: [], corrections: [] };\n }\n const definition = meta.definition;\n const stored = await this.client.hGetAll(this.fogTilesKey(room));\n const invalidStored: Record<string, string> = {};\n for (const [field, raw] of Object.entries(stored)) {\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n invalidStored[field] = raw;\n continue;\n }\n if (!isValidFogSnapshot({ meta, tiles: [parsed] })) invalidStored[field] = raw;\n }\n if (records.some((tile) => !isValidFogSnapshot({ meta, tiles: [tile] }))) {\n return {\n accepted: [],\n corrections: records.map((tile) => {\n const raw = stored[`${tile.x},${tile.y}`];\n if (raw) {\n try {\n const parsed: unknown = JSON.parse(raw);\n if (isValidFogSnapshot({ meta, tiles: [parsed] })) return parsed as FogTileRecord;\n } catch {\n // Fall through to an authoritative tombstone.\n }\n }\n return {\n generation: definition.generation,\n x: tile.x,\n y: tile.y,\n version: 1,\n editor: 'hub',\n };\n }),\n };\n }\n const result = await this.evalFog(FOG_PATCH_LWW_SCRIPT, room, records, [\n metaRaw,\n JSON.stringify(invalidStored),\n ]);\n if (Array.isArray(result) && result[0] === 2) continue;\n return parseFogRedisPatchResult(result);\n }\n throw new Error('Redis fog patch did not converge after concurrent definition writes');\n }\n\n private async evalFog(\n script: string,\n room: string,\n record: object,\n extraArguments: string[] = [],\n ): Promise<unknown> {\n if (!this.client.eval) {\n throw new Error('Redis fog persistence requires a Redis client with EVAL support');\n }\n return this.client.eval(script, {\n keys: [this.fogMetaKey(room), this.fogTilesKey(room)],\n arguments: [JSON.stringify(record), ...extraArguments],\n });\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":[]}
package/dist/index.js CHANGED
@@ -6,7 +6,14 @@ import {
6
6
  isValidFogTileRecord,
7
7
  isValidFogSnapshot
8
8
  } from "@fieldnotes/sync";
9
- import { canonicalizeFogTile } from "@fieldnotes/vtt";
9
+ import {
10
+ canonicalizeFogTile,
11
+ tileIntersectsDefinition,
12
+ parseFogRedisMetaResult,
13
+ parseFogRedisPatchResult,
14
+ FOG_META_LWW_SCRIPT,
15
+ FOG_PATCH_LWW_SCRIPT
16
+ } from "@fieldnotes/vtt";
10
17
  var RedisHubBackend = class {
11
18
  sharedAcrossInstances = true;
12
19
  client;
@@ -164,7 +171,7 @@ var RedisHubBackend = class {
164
171
  JSON.stringify(replacements)
165
172
  ]);
166
173
  if (Array.isArray(result) && result[0] === 2) continue;
167
- return parseApplyResult(result, isValidFogMetaRecord);
174
+ return parseFogRedisMetaResult(result, isValidFogMetaRecord);
168
175
  }
169
176
  throw new Error("Redis fog meta update did not converge after concurrent writes");
170
177
  }
@@ -226,7 +233,7 @@ var RedisHubBackend = class {
226
233
  JSON.stringify(invalidStored)
227
234
  ]);
228
235
  if (Array.isArray(result) && result[0] === 2) continue;
229
- return parsePatchApplyResult(result);
236
+ return parseFogRedisPatchResult(result);
230
237
  }
231
238
  throw new Error("Redis fog patch did not converge after concurrent definition writes");
232
239
  }
@@ -240,229 +247,6 @@ var RedisHubBackend = class {
240
247
  });
241
248
  }
242
249
  };
243
- function tileIntersectsDefinition(x, y, definition) {
244
- const tileWorldSize = 128 * definition.cellSize;
245
- const tileWorldX = x * tileWorldSize;
246
- const tileWorldY = y * tileWorldSize;
247
- return !(tileWorldX + tileWorldSize <= definition.bounds.x || tileWorldY + tileWorldSize <= definition.bounds.y || tileWorldX >= definition.bounds.x + definition.bounds.w || tileWorldY >= definition.bounds.y + definition.bounds.h);
248
- }
249
- function parseApplyResult(raw, guard) {
250
- if (!Array.isArray(raw) || raw[0] !== 0 && raw[0] !== 1) {
251
- throw new Error("Redis returned an invalid fog apply result");
252
- }
253
- if (raw[0] === 1) return { accepted: true };
254
- if (typeof raw[1] !== "string" || raw[1].length === 0) return { accepted: false };
255
- let correction;
256
- try {
257
- correction = JSON.parse(raw[1]);
258
- } catch {
259
- throw new Error("Redis returned an invalid fog correction");
260
- }
261
- if (!guard(correction)) throw new Error("Redis returned an invalid fog correction");
262
- return { accepted: false, correction };
263
- }
264
- function parsePatchApplyResult(raw) {
265
- if (!Array.isArray(raw) || typeof raw[0] !== "number") {
266
- throw new Error("Redis returned an invalid fog patch result");
267
- }
268
- const acceptedCount = raw[0];
269
- if (!Number.isSafeInteger(acceptedCount) || acceptedCount < 0) {
270
- throw new Error("Redis returned an invalid fog patch result");
271
- }
272
- const accepted = [];
273
- let cursor = 1;
274
- for (let i = 0; i < acceptedCount; i++, cursor++) {
275
- const parsed = parseFogTileResultRecord(raw[cursor]);
276
- accepted.push(parsed);
277
- }
278
- const correctionCount = raw[cursor++];
279
- if (!Number.isSafeInteger(correctionCount) || correctionCount < 0) {
280
- throw new Error("Redis returned an invalid fog patch result");
281
- }
282
- const corrections = [];
283
- for (let i = 0; i < correctionCount; i++, cursor++) {
284
- corrections.push(parseFogTileResultRecord(raw[cursor]));
285
- }
286
- if (cursor !== raw.length) throw new Error("Redis returned an invalid fog patch result");
287
- return { accepted, corrections };
288
- }
289
- function parseFogTileResultRecord(raw) {
290
- if (typeof raw !== "string") throw new Error("Redis returned an invalid fog patch record");
291
- let parsed;
292
- try {
293
- parsed = JSON.parse(raw);
294
- } catch {
295
- throw new Error("Redis returned an invalid fog patch record");
296
- }
297
- if (!isValidFogTileRecord(parsed)) throw new Error("Redis returned an invalid fog patch record");
298
- return parsed;
299
- }
300
- var FOG_META_LWW_SCRIPT = `
301
- local incomingRaw = ARGV[1]
302
- local incoming = cjson.decode(incomingRaw)
303
- local currentRaw = redis.call('HGET', KEYS[1], 'current')
304
- local expectedMetaRaw = ARGV[2]
305
- if (currentRaw or '') ~= expectedMetaRaw then return {2} end
306
- local expectedTiles = cjson.decode(ARGV[3])
307
- local expectedTileCount = 0
308
- for field, raw in pairs(expectedTiles) do
309
- expectedTileCount = expectedTileCount + 1
310
- if redis.call('HGET', KEYS[2], field) ~= raw then return {2} end
311
- end
312
- if redis.call('HLEN', KEYS[2]) ~= expectedTileCount then return {2} end
313
- local replacementTiles = cjson.decode(ARGV[4])
314
- local function ascii(value)
315
- if type(value) ~= 'string' or #value < 1 or #value > 128 then return false end
316
- for i = 1, #value do
317
- local b = string.byte(value, i)
318
- if b < 32 or b > 126 then return false end
319
- end
320
- return true
321
- end
322
- local function integer(value)
323
- return type(value) == 'number' and value >= 1 and value <= 9007199254740991
324
- and value == math.floor(value)
325
- end
326
- local function validDef(def)
327
- return type(def) == 'table' and def.version == 1 and ascii(def.generation)
328
- and type(def.bounds) == 'table' and type(def.bounds.x) == 'number'
329
- and type(def.bounds.y) == 'number' and type(def.bounds.w) == 'number' and def.bounds.w > 0
330
- and type(def.bounds.h) == 'number' and def.bounds.h > 0 and type(def.cellSize) == 'number'
331
- and def.cellSize > 0 and def.tileCells == 128 and (def.base == 'covered' or def.base == 'revealed')
332
- end
333
- local function validMeta(value)
334
- return type(value) == 'table' and integer(value.version) and ascii(value.editor)
335
- and (value.definition == nil or validDef(value.definition))
336
- end
337
- local current = nil
338
- if currentRaw then
339
- local ok, decoded = pcall(cjson.decode, currentRaw)
340
- if ok and validMeta(decoded) then current = decoded else redis.call('HDEL', KEYS[1], 'current') end
341
- end
342
- local function newer(a, b)
343
- return a.version > b.version or (a.version == b.version and a.editor > b.editor)
344
- end
345
- if current and not newer(incoming, current) then return {0, currentRaw} end
346
- local oldDef = current and current.definition or nil
347
- local newDef = incoming.definition
348
- if oldDef and newDef and oldDef.generation == newDef.generation
349
- and (oldDef.cellSize ~= newDef.cellSize or oldDef.tileCells ~= newDef.tileCells
350
- or oldDef.base ~= newDef.base or newDef.bounds.x > oldDef.bounds.x
351
- or newDef.bounds.y > oldDef.bounds.y
352
- or newDef.bounds.x + newDef.bounds.w < oldDef.bounds.x + oldDef.bounds.w
353
- or newDef.bounds.y + newDef.bounds.h < oldDef.bounds.y + oldDef.bounds.h) then
354
- return {0, currentRaw}
355
- end
356
-
357
- redis.call('HSET', KEYS[1], 'current', incomingRaw)
358
- redis.call('DEL', KEYS[2])
359
- if newDef and oldDef and oldDef.generation == newDef.generation then
360
- for i = 1, #replacementTiles do
361
- local tile = replacementTiles[i]
362
- redis.call('HSET', KEYS[2], tostring(tile.x) .. ',' .. tostring(tile.y), cjson.encode(tile))
363
- end
364
- end
365
- return {1}
366
- `;
367
- var FOG_PATCH_LWW_SCRIPT = `
368
- local incomingRaws = cjson.decode(ARGV[1])
369
- local metaRaw = redis.call('HGET', KEYS[1], 'current')
370
- if (metaRaw or '') ~= ARGV[2] then return {2} end
371
- local invalidStored = cjson.decode(ARGV[3])
372
- for field, raw in pairs(invalidStored) do
373
- if redis.call('HGET', KEYS[2], field) == raw then redis.call('HDEL', KEYS[2], field) end
374
- end
375
- if not metaRaw then return {0, 0} end
376
- local metaOk, meta = pcall(cjson.decode, metaRaw)
377
- if not metaOk or type(meta) ~= 'table' or type(meta.definition) ~= 'table'
378
- or type(meta.definition.generation) ~= 'string' then return {0, 0} end
379
- local def = meta.definition
380
- local tileSize = 128 * def.cellSize
381
- local function intersects(tile)
382
- local tx = tile.x * tileSize
383
- local ty = tile.y * tileSize
384
- return tx + tileSize > def.bounds.x and ty + tileSize > def.bounds.y
385
- and tx < def.bounds.x + def.bounds.w and ty < def.bounds.y + def.bounds.h
386
- end
387
- local function newer(a, b)
388
- return a.version > b.version or (a.version == b.version and a.editor > b.editor)
389
- end
390
- local function validTile(tile)
391
- if type(tile) ~= 'table' then return false end
392
- local function ascii(value)
393
- if type(value) ~= 'string' or #value < 1 or #value > 128 then return false end
394
- for i = 1, #value do
395
- local b = string.byte(value, i)
396
- if b < 32 or b > 126 then return false end
397
- end
398
- return true
399
- end
400
- local dataOk = tile.data == nil or (type(tile.data) == 'string' and #tile.data == 2732
401
- and string.match(tile.data, '^[A-Za-z0-9+/]+[AEIMQUYcgkosw048]=$') ~= nil)
402
- return ascii(tile.generation)
403
- and type(tile.x) == 'number' and math.abs(tile.x) <= 9007199254740991
404
- and tile.x == math.floor(tile.x)
405
- and type(tile.y) == 'number' and math.abs(tile.y) <= 9007199254740991
406
- and tile.y == math.floor(tile.y)
407
- and type(tile.version) == 'number' and tile.version >= 1
408
- and tile.version <= 9007199254740991 and tile.version == math.floor(tile.version)
409
- and ascii(tile.editor) and dataOk
410
- end
411
- local stored = redis.call('HGETALL', KEYS[2])
412
- local validCount = 0
413
- for i = 1, #stored, 2 do
414
- local ok, tile = pcall(cjson.decode, stored[i + 1])
415
- if not ok or not validTile(tile) or stored[i] ~= tostring(tile.x) .. ',' .. tostring(tile.y)
416
- or tile.generation ~= def.generation or not intersects(tile) then
417
- redis.call('HDEL', KEYS[2], stored[i])
418
- else
419
- validCount = validCount + 1
420
- end
421
- end
422
- local accepted = {}
423
- local corrections = {}
424
- local newCount = 0
425
- local currents = {}
426
- for i = 1, #incomingRaws do
427
- local incomingRaw = cjson.encode(incomingRaws[i])
428
- local incoming = incomingRaws[i]
429
- local field = tostring(incoming.x) .. ',' .. tostring(incoming.y)
430
- local currentRaw = redis.call('HGET', KEYS[2], field)
431
- local current = nil
432
- if currentRaw then
433
- local ok, decoded = pcall(cjson.decode, currentRaw)
434
- if ok and validTile(decoded) then current = decoded else currentRaw = nil end
435
- end
436
- currents[i] = currentRaw or false
437
- if incoming.generation ~= def.generation or not intersects(incoming)
438
- or (current and not newer(incoming, current)) then
439
- corrections[#corrections + 1] = currentRaw or cjson.encode({generation=def.generation,
440
- x=incoming.x, y=incoming.y, version=1, editor='hub'})
441
- else
442
- accepted[#accepted + 1] = incomingRaw
443
- if not current then newCount = newCount + 1 end
444
- end
445
- end
446
- if validCount + newCount > 256 then
447
- accepted = {}
448
- corrections = {}
449
- for i = 1, #incomingRaws do
450
- local incoming = incomingRaws[i]
451
- corrections[#corrections + 1] = currents[i] or cjson.encode({generation=def.generation,
452
- x=incoming.x, y=incoming.y, version=1, editor='hub'})
453
- end
454
- else
455
- for i = 1, #accepted do
456
- local record = cjson.decode(accepted[i])
457
- redis.call('HSET', KEYS[2], tostring(record.x) .. ',' .. tostring(record.y), accepted[i])
458
- end
459
- end
460
- local result = {#accepted}
461
- for i = 1, #accepted do result[#result + 1] = accepted[i] end
462
- result[#result + 1] = #corrections
463
- for i = 1, #corrections do result[#result + 1] = corrections[i] end
464
- return result
465
- `;
466
250
 
467
251
  // src/redis-hub-fanout.ts
468
252
  var RedisHubFanout = class {
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/redis-hub-backend.ts","../src/redis-hub-fanout.ts"],"sourcesContent":["import {\n isValidElement,\n isValidLayerRecord,\n isValidFogMetaRecord,\n isValidFogTileRecord,\n isValidFogSnapshot,\n type LayerRecord,\n type SyncOp,\n type FogSnapshot,\n type FogMetaRecord,\n type FogTileRecord,\n} from '@fieldnotes/sync';\nimport { canonicalizeFogTile } from '@fieldnotes/vtt';\nimport type { CanvasElement } from '@fieldnotes/core';\nimport type { FogApplyResult, FogPatchApplyResult, 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 readonly sharedAcrossInstances = true;\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 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 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 // 'clear' deletes elements only; the layer ledger is a separate hash and survives.\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 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 let parsed: unknown;\n try {\n parsed = JSON.parse(value);\n } catch {\n continue; // skip a corrupt stored value rather than throwing the whole ledger\n }\n if (isValidLayerRecord(parsed)) out.push(parsed);\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 private fogMetaKey(room: string): string {\n return `${this.keyPrefix}${room}:fog:meta`;\n }\n\n private fogTilesKey(room: string): string {\n return `${this.keyPrefix}${room}:fog:tiles`;\n }\n\n async fogSnapshot(room: string): Promise<FogSnapshot | undefined> {\n const metaStr = await this.client.hGet(this.fogMetaKey(room), 'current');\n if (metaStr == null) return undefined;\n let meta: unknown;\n try {\n meta = JSON.parse(metaStr);\n } catch {\n return undefined;\n }\n if (!isValidFogMetaRecord(meta)) return undefined;\n\n if (!(meta as FogMetaRecord).definition) return { meta: meta as FogMetaRecord, tiles: [] };\n\n const definition = (meta as FogMetaRecord).definition;\n if (!definition) return { meta: meta as FogMetaRecord, tiles: [] };\n const tileMap = await this.client.hGetAll(this.fogTilesKey(room));\n const tiles: FogTileRecord[] = [];\n const seen = new Set<string>();\n for (const value of Object.values(tileMap)) {\n let parsed: unknown;\n try {\n parsed = JSON.parse(value);\n } catch {\n continue;\n }\n if (!isValidFogTileRecord(parsed)) continue;\n if (parsed.generation !== definition.generation) continue;\n if (!tileIntersectsDefinition(parsed.x, parsed.y, definition)) continue;\n if (!isValidFogSnapshot({ meta: meta as FogMetaRecord, tiles: [parsed] })) continue;\n const key = `${parsed.x},${parsed.y}`;\n if (seen.has(key)) continue;\n seen.add(key);\n tiles.push(parsed);\n if (tiles.length === 256) break;\n }\n\n const snapshot = { meta: meta as FogMetaRecord, tiles };\n return isValidFogSnapshot(snapshot) ? snapshot : undefined;\n }\n\n async applyFogMeta(room: string, record: FogMetaRecord): Promise<FogApplyResult<FogMetaRecord>> {\n for (let attempt = 0; attempt < 4; attempt++) {\n const metaKey = this.fogMetaKey(room);\n const tilesKey = this.fogTilesKey(room);\n const expectedMeta = await this.client.hGet(metaKey, 'current');\n const expectedTiles = await this.client.hGetAll(tilesKey);\n const replacements: FogTileRecord[] = [];\n let current: FogMetaRecord | undefined;\n if (expectedMeta !== null) {\n try {\n const parsed: unknown = JSON.parse(expectedMeta);\n if (isValidFogMetaRecord(parsed)) current = parsed;\n } catch {\n // Corrupt state is atomically replaced by a valid winning record.\n }\n }\n if (\n current?.definition &&\n record.definition &&\n current.definition.generation === record.definition.generation\n ) {\n for (const raw of Object.values(expectedTiles)) {\n let tile: unknown;\n try {\n tile = JSON.parse(raw);\n } catch {\n continue;\n }\n if (!isValidFogSnapshot({ meta: current, tiles: [tile] })) continue;\n const valid = tile as FogTileRecord;\n if (!tileIntersectsDefinition(valid.x, valid.y, record.definition)) continue;\n if (valid.data === undefined) {\n replacements.push(valid);\n continue;\n }\n const canonical = canonicalizeFogTile(\n { x: valid.x, y: valid.y, data: valid.data },\n record.definition,\n );\n if (canonical) replacements.push({ ...valid, data: canonical.data });\n }\n }\n const result = await this.evalFog(FOG_META_LWW_SCRIPT, room, record, [\n expectedMeta ?? '',\n JSON.stringify(expectedTiles),\n JSON.stringify(replacements),\n ]);\n if (Array.isArray(result) && result[0] === 2) continue;\n return parseApplyResult(result, isValidFogMetaRecord);\n }\n throw new Error('Redis fog meta update did not converge after concurrent writes');\n }\n\n async applyFogTile(room: string, record: FogTileRecord): Promise<FogApplyResult<FogTileRecord>> {\n const result = await this.applyFogPatch(room, [record]);\n if (result.accepted.length === 1) return { accepted: true };\n return { accepted: false, correction: result.corrections[0] };\n }\n\n async applyFogPatch(\n room: string,\n records: readonly FogTileRecord[],\n ): Promise<FogPatchApplyResult> {\n for (let attempt = 0; attempt < 4; attempt++) {\n const metaRaw = await this.client.hGet(this.fogMetaKey(room), 'current');\n if (metaRaw === null) return { accepted: [], corrections: [] };\n let meta: unknown;\n try {\n meta = JSON.parse(metaRaw);\n } catch {\n return { accepted: [], corrections: [] };\n }\n if (!isValidFogMetaRecord(meta) || !meta.definition) {\n return { accepted: [], corrections: [] };\n }\n const definition = meta.definition;\n const stored = await this.client.hGetAll(this.fogTilesKey(room));\n const invalidStored: Record<string, string> = {};\n for (const [field, raw] of Object.entries(stored)) {\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n invalidStored[field] = raw;\n continue;\n }\n if (!isValidFogSnapshot({ meta, tiles: [parsed] })) invalidStored[field] = raw;\n }\n if (records.some((tile) => !isValidFogSnapshot({ meta, tiles: [tile] }))) {\n return {\n accepted: [],\n corrections: records.map((tile) => {\n const raw = stored[`${tile.x},${tile.y}`];\n if (raw) {\n try {\n const parsed: unknown = JSON.parse(raw);\n if (isValidFogSnapshot({ meta, tiles: [parsed] })) return parsed as FogTileRecord;\n } catch {\n // Fall through to an authoritative tombstone.\n }\n }\n return {\n generation: definition.generation,\n x: tile.x,\n y: tile.y,\n version: 1,\n editor: 'hub',\n };\n }),\n };\n }\n const result = await this.evalFog(FOG_PATCH_LWW_SCRIPT, room, records, [\n metaRaw,\n JSON.stringify(invalidStored),\n ]);\n if (Array.isArray(result) && result[0] === 2) continue;\n return parsePatchApplyResult(result);\n }\n throw new Error('Redis fog patch did not converge after concurrent definition writes');\n }\n\n private async evalFog(\n script: string,\n room: string,\n record: object,\n extraArguments: string[] = [],\n ): Promise<unknown> {\n if (!this.client.eval) {\n throw new Error('Redis fog persistence requires a Redis client with EVAL support');\n }\n return this.client.eval(script, {\n keys: [this.fogMetaKey(room), this.fogTilesKey(room)],\n arguments: [JSON.stringify(record), ...extraArguments],\n });\n }\n}\n\nfunction tileIntersectsDefinition(\n x: number,\n y: number,\n definition: NonNullable<FogMetaRecord['definition']>,\n): boolean {\n const tileWorldSize = 128 * definition.cellSize;\n const tileWorldX = x * tileWorldSize;\n const tileWorldY = y * tileWorldSize;\n return !(\n tileWorldX + tileWorldSize <= definition.bounds.x ||\n tileWorldY + tileWorldSize <= definition.bounds.y ||\n tileWorldX >= definition.bounds.x + definition.bounds.w ||\n tileWorldY >= definition.bounds.y + definition.bounds.h\n );\n}\n\nfunction parseApplyResult<T>(\n raw: unknown,\n guard: (value: unknown) => value is T,\n): FogApplyResult<T> {\n if (!Array.isArray(raw) || (raw[0] !== 0 && raw[0] !== 1)) {\n throw new Error('Redis returned an invalid fog apply result');\n }\n if (raw[0] === 1) return { accepted: true };\n if (typeof raw[1] !== 'string' || raw[1].length === 0) return { accepted: false };\n let correction: unknown;\n try {\n correction = JSON.parse(raw[1]);\n } catch {\n throw new Error('Redis returned an invalid fog correction');\n }\n if (!guard(correction)) throw new Error('Redis returned an invalid fog correction');\n return { accepted: false, correction };\n}\n\nfunction parsePatchApplyResult(raw: unknown): FogPatchApplyResult {\n if (!Array.isArray(raw) || typeof raw[0] !== 'number') {\n throw new Error('Redis returned an invalid fog patch result');\n }\n const acceptedCount = raw[0];\n if (!Number.isSafeInteger(acceptedCount) || acceptedCount < 0) {\n throw new Error('Redis returned an invalid fog patch result');\n }\n const accepted: FogTileRecord[] = [];\n let cursor = 1;\n for (let i = 0; i < acceptedCount; i++, cursor++) {\n const parsed = parseFogTileResultRecord(raw[cursor]);\n accepted.push(parsed);\n }\n const correctionCount = raw[cursor++];\n if (!Number.isSafeInteger(correctionCount) || (correctionCount as number) < 0) {\n throw new Error('Redis returned an invalid fog patch result');\n }\n const corrections: FogTileRecord[] = [];\n for (let i = 0; i < (correctionCount as number); i++, cursor++) {\n corrections.push(parseFogTileResultRecord(raw[cursor]));\n }\n if (cursor !== raw.length) throw new Error('Redis returned an invalid fog patch result');\n return { accepted, corrections };\n}\n\nfunction parseFogTileResultRecord(raw: unknown): FogTileRecord {\n if (typeof raw !== 'string') throw new Error('Redis returned an invalid fog patch record');\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n throw new Error('Redis returned an invalid fog patch record');\n }\n if (!isValidFogTileRecord(parsed)) throw new Error('Redis returned an invalid fog patch record');\n return parsed;\n}\n\nconst FOG_META_LWW_SCRIPT = `\nlocal incomingRaw = ARGV[1]\nlocal incoming = cjson.decode(incomingRaw)\nlocal currentRaw = redis.call('HGET', KEYS[1], 'current')\nlocal expectedMetaRaw = ARGV[2]\nif (currentRaw or '') ~= expectedMetaRaw then return {2} end\nlocal expectedTiles = cjson.decode(ARGV[3])\nlocal expectedTileCount = 0\nfor field, raw in pairs(expectedTiles) do\n expectedTileCount = expectedTileCount + 1\n if redis.call('HGET', KEYS[2], field) ~= raw then return {2} end\nend\nif redis.call('HLEN', KEYS[2]) ~= expectedTileCount then return {2} end\nlocal replacementTiles = cjson.decode(ARGV[4])\nlocal function ascii(value)\n if type(value) ~= 'string' or #value < 1 or #value > 128 then return false end\n for i = 1, #value do\n local b = string.byte(value, i)\n if b < 32 or b > 126 then return false end\n end\n return true\nend\nlocal function integer(value)\n return type(value) == 'number' and value >= 1 and value <= 9007199254740991\n and value == math.floor(value)\nend\nlocal function validDef(def)\n return type(def) == 'table' and def.version == 1 and ascii(def.generation)\n and type(def.bounds) == 'table' and type(def.bounds.x) == 'number'\n and type(def.bounds.y) == 'number' and type(def.bounds.w) == 'number' and def.bounds.w > 0\n and type(def.bounds.h) == 'number' and def.bounds.h > 0 and type(def.cellSize) == 'number'\n and def.cellSize > 0 and def.tileCells == 128 and (def.base == 'covered' or def.base == 'revealed')\nend\nlocal function validMeta(value)\n return type(value) == 'table' and integer(value.version) and ascii(value.editor)\n and (value.definition == nil or validDef(value.definition))\nend\nlocal current = nil\nif currentRaw then\n local ok, decoded = pcall(cjson.decode, currentRaw)\n if ok and validMeta(decoded) then current = decoded else redis.call('HDEL', KEYS[1], 'current') end\nend\nlocal function newer(a, b)\n return a.version > b.version or (a.version == b.version and a.editor > b.editor)\nend\nif current and not newer(incoming, current) then return {0, currentRaw} end\nlocal oldDef = current and current.definition or nil\nlocal newDef = incoming.definition\nif oldDef and newDef and oldDef.generation == newDef.generation\n and (oldDef.cellSize ~= newDef.cellSize or oldDef.tileCells ~= newDef.tileCells\n or oldDef.base ~= newDef.base or newDef.bounds.x > oldDef.bounds.x\n or newDef.bounds.y > oldDef.bounds.y\n or newDef.bounds.x + newDef.bounds.w < oldDef.bounds.x + oldDef.bounds.w\n or newDef.bounds.y + newDef.bounds.h < oldDef.bounds.y + oldDef.bounds.h) then\n return {0, currentRaw}\nend\n\nredis.call('HSET', KEYS[1], 'current', incomingRaw)\nredis.call('DEL', KEYS[2])\nif newDef and oldDef and oldDef.generation == newDef.generation then\n for i = 1, #replacementTiles do\n local tile = replacementTiles[i]\n redis.call('HSET', KEYS[2], tostring(tile.x) .. ',' .. tostring(tile.y), cjson.encode(tile))\n end\nend\nreturn {1}\n`;\n\nconst FOG_PATCH_LWW_SCRIPT = `\nlocal incomingRaws = cjson.decode(ARGV[1])\nlocal metaRaw = redis.call('HGET', KEYS[1], 'current')\nif (metaRaw or '') ~= ARGV[2] then return {2} end\nlocal invalidStored = cjson.decode(ARGV[3])\nfor field, raw in pairs(invalidStored) do\n if redis.call('HGET', KEYS[2], field) == raw then redis.call('HDEL', KEYS[2], field) end\nend\nif not metaRaw then return {0, 0} end\nlocal metaOk, meta = pcall(cjson.decode, metaRaw)\nif not metaOk or type(meta) ~= 'table' or type(meta.definition) ~= 'table'\n or type(meta.definition.generation) ~= 'string' then return {0, 0} end\nlocal def = meta.definition\nlocal tileSize = 128 * def.cellSize\nlocal function intersects(tile)\n local tx = tile.x * tileSize\n local ty = tile.y * tileSize\n return tx + tileSize > def.bounds.x and ty + tileSize > def.bounds.y\n and tx < def.bounds.x + def.bounds.w and ty < def.bounds.y + def.bounds.h\nend\nlocal function newer(a, b)\n return a.version > b.version or (a.version == b.version and a.editor > b.editor)\nend\nlocal function validTile(tile)\n if type(tile) ~= 'table' then return false end\n local function ascii(value)\n if type(value) ~= 'string' or #value < 1 or #value > 128 then return false end\n for i = 1, #value do\n local b = string.byte(value, i)\n if b < 32 or b > 126 then return false end\n end\n return true\n end\n local dataOk = tile.data == nil or (type(tile.data) == 'string' and #tile.data == 2732\n and string.match(tile.data, '^[A-Za-z0-9+/]+[AEIMQUYcgkosw048]=$') ~= nil)\n return ascii(tile.generation)\n and type(tile.x) == 'number' and math.abs(tile.x) <= 9007199254740991\n and tile.x == math.floor(tile.x)\n and type(tile.y) == 'number' and math.abs(tile.y) <= 9007199254740991\n and tile.y == math.floor(tile.y)\n and type(tile.version) == 'number' and tile.version >= 1\n and tile.version <= 9007199254740991 and tile.version == math.floor(tile.version)\n and ascii(tile.editor) and dataOk\nend\nlocal stored = redis.call('HGETALL', KEYS[2])\nlocal validCount = 0\nfor i = 1, #stored, 2 do\n local ok, tile = pcall(cjson.decode, stored[i + 1])\n if not ok or not validTile(tile) or stored[i] ~= tostring(tile.x) .. ',' .. tostring(tile.y)\n or tile.generation ~= def.generation or not intersects(tile) then\n redis.call('HDEL', KEYS[2], stored[i])\n else\n validCount = validCount + 1\n end\nend\nlocal accepted = {}\nlocal corrections = {}\nlocal newCount = 0\nlocal currents = {}\nfor i = 1, #incomingRaws do\n local incomingRaw = cjson.encode(incomingRaws[i])\n local incoming = incomingRaws[i]\n local field = tostring(incoming.x) .. ',' .. tostring(incoming.y)\n local currentRaw = redis.call('HGET', KEYS[2], field)\n local current = nil\n if currentRaw then\n local ok, decoded = pcall(cjson.decode, currentRaw)\n if ok and validTile(decoded) then current = decoded else currentRaw = nil end\n end\n currents[i] = currentRaw or false\n if incoming.generation ~= def.generation or not intersects(incoming)\n or (current and not newer(incoming, current)) then\n corrections[#corrections + 1] = currentRaw or cjson.encode({generation=def.generation,\n x=incoming.x, y=incoming.y, version=1, editor='hub'})\n else\n accepted[#accepted + 1] = incomingRaw\n if not current then newCount = newCount + 1 end\n end\nend\nif validCount + newCount > 256 then\n accepted = {}\n corrections = {}\n for i = 1, #incomingRaws do\n local incoming = incomingRaws[i]\n corrections[#corrections + 1] = currents[i] or cjson.encode({generation=def.generation,\n x=incoming.x, y=incoming.y, version=1, editor='hub'})\n end\nelse\n for i = 1, #accepted do\n local record = cjson.decode(accepted[i])\n redis.call('HSET', KEYS[2], tostring(record.x) .. ',' .. tostring(record.y), accepted[i])\n end\nend\nlocal result = {#accepted}\nfor i = 1, #accepted do result[#result + 1] = accepted[i] end\nresult[#result + 1] = #corrections\nfor i = 1, #corrections do result[#result + 1] = corrections[i] end\nreturn result\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/redis-hub-fanout.ts"],"sourcesContent":["import {\n isValidElement,\n isValidLayerRecord,\n isValidFogMetaRecord,\n isValidFogTileRecord,\n isValidFogSnapshot,\n type LayerRecord,\n type SyncOp,\n type FogSnapshot,\n type FogMetaRecord,\n type FogTileRecord,\n} from '@fieldnotes/sync';\nimport {\n canonicalizeFogTile,\n tileIntersectsDefinition,\n parseFogRedisMetaResult,\n parseFogRedisPatchResult,\n FOG_META_LWW_SCRIPT,\n FOG_PATCH_LWW_SCRIPT,\n} from '@fieldnotes/vtt';\nimport type { CanvasElement } from '@fieldnotes/core';\nimport type { FogApplyResult, FogPatchApplyResult, 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 readonly sharedAcrossInstances = true;\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 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 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 // 'clear' deletes elements only; the layer ledger is a separate hash and survives.\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 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 let parsed: unknown;\n try {\n parsed = JSON.parse(value);\n } catch {\n continue; // skip a corrupt stored value rather than throwing the whole ledger\n }\n if (isValidLayerRecord(parsed)) out.push(parsed);\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 private fogMetaKey(room: string): string {\n return `${this.keyPrefix}${room}:fog:meta`;\n }\n\n private fogTilesKey(room: string): string {\n return `${this.keyPrefix}${room}:fog:tiles`;\n }\n\n async fogSnapshot(room: string): Promise<FogSnapshot | undefined> {\n const metaStr = await this.client.hGet(this.fogMetaKey(room), 'current');\n if (metaStr == null) return undefined;\n let meta: unknown;\n try {\n meta = JSON.parse(metaStr);\n } catch {\n return undefined;\n }\n if (!isValidFogMetaRecord(meta)) return undefined;\n\n if (!(meta as FogMetaRecord).definition) return { meta: meta as FogMetaRecord, tiles: [] };\n\n const definition = (meta as FogMetaRecord).definition;\n if (!definition) return { meta: meta as FogMetaRecord, tiles: [] };\n const tileMap = await this.client.hGetAll(this.fogTilesKey(room));\n const tiles: FogTileRecord[] = [];\n const seen = new Set<string>();\n for (const value of Object.values(tileMap)) {\n let parsed: unknown;\n try {\n parsed = JSON.parse(value);\n } catch {\n continue;\n }\n if (!isValidFogTileRecord(parsed)) continue;\n if (parsed.generation !== definition.generation) continue;\n if (!tileIntersectsDefinition(parsed.x, parsed.y, definition)) continue;\n if (!isValidFogSnapshot({ meta: meta as FogMetaRecord, tiles: [parsed] })) continue;\n const key = `${parsed.x},${parsed.y}`;\n if (seen.has(key)) continue;\n seen.add(key);\n tiles.push(parsed);\n if (tiles.length === 256) break;\n }\n\n const snapshot = { meta: meta as FogMetaRecord, tiles };\n return isValidFogSnapshot(snapshot) ? snapshot : undefined;\n }\n\n async applyFogMeta(room: string, record: FogMetaRecord): Promise<FogApplyResult<FogMetaRecord>> {\n for (let attempt = 0; attempt < 4; attempt++) {\n const metaKey = this.fogMetaKey(room);\n const tilesKey = this.fogTilesKey(room);\n const expectedMeta = await this.client.hGet(metaKey, 'current');\n const expectedTiles = await this.client.hGetAll(tilesKey);\n const replacements: FogTileRecord[] = [];\n let current: FogMetaRecord | undefined;\n if (expectedMeta !== null) {\n try {\n const parsed: unknown = JSON.parse(expectedMeta);\n if (isValidFogMetaRecord(parsed)) current = parsed;\n } catch {\n // Corrupt state is atomically replaced by a valid winning record.\n }\n }\n if (\n current?.definition &&\n record.definition &&\n current.definition.generation === record.definition.generation\n ) {\n for (const raw of Object.values(expectedTiles)) {\n let tile: unknown;\n try {\n tile = JSON.parse(raw);\n } catch {\n continue;\n }\n if (!isValidFogSnapshot({ meta: current, tiles: [tile] })) continue;\n const valid = tile as FogTileRecord;\n if (!tileIntersectsDefinition(valid.x, valid.y, record.definition)) continue;\n if (valid.data === undefined) {\n replacements.push(valid);\n continue;\n }\n const canonical = canonicalizeFogTile(\n { x: valid.x, y: valid.y, data: valid.data },\n record.definition,\n );\n if (canonical) replacements.push({ ...valid, data: canonical.data });\n }\n }\n const result = await this.evalFog(FOG_META_LWW_SCRIPT, room, record, [\n expectedMeta ?? '',\n JSON.stringify(expectedTiles),\n JSON.stringify(replacements),\n ]);\n if (Array.isArray(result) && result[0] === 2) continue;\n return parseFogRedisMetaResult(result, isValidFogMetaRecord);\n }\n throw new Error('Redis fog meta update did not converge after concurrent writes');\n }\n\n async applyFogTile(room: string, record: FogTileRecord): Promise<FogApplyResult<FogTileRecord>> {\n const result = await this.applyFogPatch(room, [record]);\n if (result.accepted.length === 1) return { accepted: true };\n return { accepted: false, correction: result.corrections[0] };\n }\n\n async applyFogPatch(\n room: string,\n records: readonly FogTileRecord[],\n ): Promise<FogPatchApplyResult> {\n for (let attempt = 0; attempt < 4; attempt++) {\n const metaRaw = await this.client.hGet(this.fogMetaKey(room), 'current');\n if (metaRaw === null) return { accepted: [], corrections: [] };\n let meta: unknown;\n try {\n meta = JSON.parse(metaRaw);\n } catch {\n return { accepted: [], corrections: [] };\n }\n if (!isValidFogMetaRecord(meta) || !meta.definition) {\n return { accepted: [], corrections: [] };\n }\n const definition = meta.definition;\n const stored = await this.client.hGetAll(this.fogTilesKey(room));\n const invalidStored: Record<string, string> = {};\n for (const [field, raw] of Object.entries(stored)) {\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n invalidStored[field] = raw;\n continue;\n }\n if (!isValidFogSnapshot({ meta, tiles: [parsed] })) invalidStored[field] = raw;\n }\n if (records.some((tile) => !isValidFogSnapshot({ meta, tiles: [tile] }))) {\n return {\n accepted: [],\n corrections: records.map((tile) => {\n const raw = stored[`${tile.x},${tile.y}`];\n if (raw) {\n try {\n const parsed: unknown = JSON.parse(raw);\n if (isValidFogSnapshot({ meta, tiles: [parsed] })) return parsed as FogTileRecord;\n } catch {\n // Fall through to an authoritative tombstone.\n }\n }\n return {\n generation: definition.generation,\n x: tile.x,\n y: tile.y,\n version: 1,\n editor: 'hub',\n };\n }),\n };\n }\n const result = await this.evalFog(FOG_PATCH_LWW_SCRIPT, room, records, [\n metaRaw,\n JSON.stringify(invalidStored),\n ]);\n if (Array.isArray(result) && result[0] === 2) continue;\n return parseFogRedisPatchResult(result);\n }\n throw new Error('Redis fog patch did not converge after concurrent definition writes');\n }\n\n private async evalFog(\n script: string,\n room: string,\n record: object,\n extraArguments: string[] = [],\n ): Promise<unknown> {\n if (!this.client.eval) {\n throw new Error('Redis fog persistence requires a Redis client with EVAL support');\n }\n return this.client.eval(script, {\n keys: [this.fogMetaKey(room), this.fogTilesKey(room)],\n arguments: [JSON.stringify(record), ...extraArguments],\n });\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":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fieldnotes/sync-redis",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
4
4
  "description": "Redis-backed HubBackend for Field Notes real-time sync relay",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -36,15 +36,15 @@
36
36
  "fieldnotes"
37
37
  ],
38
38
  "dependencies": {
39
- "@fieldnotes/sync": "0.15.0"
39
+ "@fieldnotes/sync": "0.16.0"
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/sync-server": "0.16.0",
46
45
  "@fieldnotes/core": "0.78.0",
47
- "@fieldnotes/vtt": "0.3.0"
46
+ "@fieldnotes/sync-server": "0.16.0",
47
+ "@fieldnotes/vtt": "0.5.0"
48
48
  },
49
49
  "scripts": {
50
50
  "build": "tsup",