@fieldnotes/sync-redis 0.5.0 → 0.7.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 +2 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +24 -1
- package/dist/index.d.ts +24 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +5 -4
package/dist/index.cjs
CHANGED
|
@@ -27,7 +27,7 @@ module.exports = __toCommonJS(index_exports);
|
|
|
27
27
|
|
|
28
28
|
// src/redis-hub-backend.ts
|
|
29
29
|
var import_sync = require("@fieldnotes/sync");
|
|
30
|
-
var
|
|
30
|
+
var import_vtt = require("@fieldnotes/vtt");
|
|
31
31
|
var RedisHubBackend = class {
|
|
32
32
|
sharedAcrossInstances = true;
|
|
33
33
|
client;
|
|
@@ -172,7 +172,7 @@ var RedisHubBackend = class {
|
|
|
172
172
|
replacements.push(valid);
|
|
173
173
|
continue;
|
|
174
174
|
}
|
|
175
|
-
const canonical = (0,
|
|
175
|
+
const canonical = (0, import_vtt.canonicalizeFogTile)(
|
|
176
176
|
{ x: valid.x, y: valid.y, data: valid.data },
|
|
177
177
|
record.definition
|
|
178
178
|
);
|
package/dist/index.cjs.map
CHANGED
|
@@ -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';\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, 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 { 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":[]}
|
package/dist/index.d.cts
CHANGED
|
@@ -63,4 +63,27 @@ declare class RedisHubFanout implements HubFanout {
|
|
|
63
63
|
subscribe(handler: (payload: string) => void): () => void;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
|
|
66
|
+
interface ApplyResult {
|
|
67
|
+
accepted: SyncOp | null;
|
|
68
|
+
corrections: SyncOp[];
|
|
69
|
+
broadcast?: SyncOp[];
|
|
70
|
+
locality?: 'shared' | 'local';
|
|
71
|
+
}
|
|
72
|
+
interface BackendOpContext {
|
|
73
|
+
readonly room: string;
|
|
74
|
+
}
|
|
75
|
+
interface PluginSnapshot {
|
|
76
|
+
readonly pluginName: string;
|
|
77
|
+
readonly version: number;
|
|
78
|
+
readonly data: unknown;
|
|
79
|
+
}
|
|
80
|
+
interface BackendSyncPlugin {
|
|
81
|
+
readonly name: string;
|
|
82
|
+
readonly keyPrefix: string;
|
|
83
|
+
readonly scripts?: Record<string, string>;
|
|
84
|
+
snapshot?(room: string): Promise<PluginSnapshot>;
|
|
85
|
+
applyOp?(op: SyncOp, ctx: BackendOpContext): Promise<ApplyResult>;
|
|
86
|
+
dispose?(): void;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export { type ApplyResult, type BackendOpContext, type BackendSyncPlugin, type PluginSnapshot, type RedisHashClient, RedisHubBackend, type RedisHubBackendOptions, RedisHubFanout, type RedisHubFanoutOptions, type RedisPublisher, type RedisSubscriber };
|
package/dist/index.d.ts
CHANGED
|
@@ -63,4 +63,27 @@ declare class RedisHubFanout implements HubFanout {
|
|
|
63
63
|
subscribe(handler: (payload: string) => void): () => void;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
|
|
66
|
+
interface ApplyResult {
|
|
67
|
+
accepted: SyncOp | null;
|
|
68
|
+
corrections: SyncOp[];
|
|
69
|
+
broadcast?: SyncOp[];
|
|
70
|
+
locality?: 'shared' | 'local';
|
|
71
|
+
}
|
|
72
|
+
interface BackendOpContext {
|
|
73
|
+
readonly room: string;
|
|
74
|
+
}
|
|
75
|
+
interface PluginSnapshot {
|
|
76
|
+
readonly pluginName: string;
|
|
77
|
+
readonly version: number;
|
|
78
|
+
readonly data: unknown;
|
|
79
|
+
}
|
|
80
|
+
interface BackendSyncPlugin {
|
|
81
|
+
readonly name: string;
|
|
82
|
+
readonly keyPrefix: string;
|
|
83
|
+
readonly scripts?: Record<string, string>;
|
|
84
|
+
snapshot?(room: string): Promise<PluginSnapshot>;
|
|
85
|
+
applyOp?(op: SyncOp, ctx: BackendOpContext): Promise<ApplyResult>;
|
|
86
|
+
dispose?(): void;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export { type ApplyResult, type BackendOpContext, type BackendSyncPlugin, type PluginSnapshot, type RedisHashClient, RedisHubBackend, type RedisHubBackendOptions, RedisHubFanout, type RedisHubFanoutOptions, type RedisPublisher, type RedisSubscriber };
|
package/dist/index.js
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
isValidFogTileRecord,
|
|
7
7
|
isValidFogSnapshot
|
|
8
8
|
} from "@fieldnotes/sync";
|
|
9
|
-
import { canonicalizeFogTile } from "@fieldnotes/
|
|
9
|
+
import { canonicalizeFogTile } from "@fieldnotes/vtt";
|
|
10
10
|
var RedisHubBackend = class {
|
|
11
11
|
sharedAcrossInstances = true;
|
|
12
12
|
client;
|
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, 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 { 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":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fieldnotes/sync-redis",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Redis-backed HubBackend for Field Notes real-time sync relay",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -36,14 +36,15 @@
|
|
|
36
36
|
"fieldnotes"
|
|
37
37
|
],
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@fieldnotes/sync": "0.
|
|
39
|
+
"@fieldnotes/sync": "0.15.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/
|
|
46
|
-
"@fieldnotes/
|
|
45
|
+
"@fieldnotes/sync-server": "0.16.0",
|
|
46
|
+
"@fieldnotes/core": "0.78.0",
|
|
47
|
+
"@fieldnotes/vtt": "0.3.0"
|
|
47
48
|
},
|
|
48
49
|
"scripts": {
|
|
49
50
|
"build": "tsup",
|