@syncframe/redis 0.1.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/LICENSE +21 -0
- package/README.md +36 -0
- package/dist/index.d.ts +68 -0
- package/dist/index.js +97 -0
- package/dist/index.js.map +1 -0
- package/package.json +54 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 jkvc
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# @syncframe/redis
|
|
2
|
+
|
|
3
|
+
Redis-backed [`SyncStore`](https://www.npmjs.com/package/@syncframe/core) and `SyncTransport` adapters for [`@syncframe/core`](https://www.npmjs.com/package/@syncframe/core), so a `SyncServer` can persist anchors and fan out snapshots across processes and serverless instances.
|
|
4
|
+
|
|
5
|
+
The adapters are **connection-injected** — you pass in your own [`ioredis`](https://www.npmjs.com/package/ioredis) clients, so connection, auth, and pooling stay in your app. The dependency on core is type-only, so this package adds no runtime coupling.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @syncframe/redis @syncframe/core ioredis
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
`ioredis` is a peer dependency.
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import Redis from 'ioredis';
|
|
19
|
+
import { SyncServer } from '@syncframe/core/server';
|
|
20
|
+
import { RedisStore, RedisTransport } from '@syncframe/redis';
|
|
21
|
+
|
|
22
|
+
const redis = new Redis(process.env.REDIS_URL!);
|
|
23
|
+
|
|
24
|
+
const server = new SyncServer({
|
|
25
|
+
store: new RedisStore({ redis }),
|
|
26
|
+
// A subscriber connection can't issue other commands, so the transport
|
|
27
|
+
// creates a fresh one per subscription.
|
|
28
|
+
transport: new RedisTransport({ redis, createSubscriber: () => new Redis(process.env.REDIS_URL!) }),
|
|
29
|
+
});
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Both adapters accept a `prefix` option (default `"syncframe"`) to namespace keys and channels.
|
|
33
|
+
|
|
34
|
+
## License
|
|
35
|
+
|
|
36
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import Redis from 'ioredis';
|
|
2
|
+
import { SyncStore, AnyAnchor, SyncTransport, CoreSnapshot } from '@syncframe/core/server';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @syncframe/redis — Redis-backed adapters for @syncframe/core.
|
|
6
|
+
*
|
|
7
|
+
* Implements core's `SyncStore` and `SyncTransport` interfaces on top of Redis,
|
|
8
|
+
* so a `SyncServer` can persist anchors and fan out snapshots across processes
|
|
9
|
+
* and serverless instances.
|
|
10
|
+
*
|
|
11
|
+
* Core stays zero-dependency: these adapters only *implement* its interfaces.
|
|
12
|
+
* Every `@syncframe/core` and `ioredis` import here is type-only and erased at
|
|
13
|
+
* compile time — the package has no runtime dependency on core, and the Redis
|
|
14
|
+
* client is injected by the caller (so connection, auth, and pooling concerns
|
|
15
|
+
* live in the application, not here).
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
interface RedisStoreOptions {
|
|
19
|
+
/** Client used for all commands. */
|
|
20
|
+
redis: Redis;
|
|
21
|
+
/** Key namespace prefix. Default `"syncframe"`. */
|
|
22
|
+
prefix?: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Anchors, meta, and content data are stored as JSON under namespaced keys.
|
|
26
|
+
* The set of channel ids per room is tracked in a Redis set so `listAnchors`
|
|
27
|
+
* (and therefore snapshot building) works without key scanning.
|
|
28
|
+
*/
|
|
29
|
+
declare class RedisStore implements SyncStore {
|
|
30
|
+
private readonly redis;
|
|
31
|
+
private readonly prefix;
|
|
32
|
+
constructor(options: RedisStoreOptions);
|
|
33
|
+
private ns;
|
|
34
|
+
private anchorKey;
|
|
35
|
+
private channelsKey;
|
|
36
|
+
private metaKey;
|
|
37
|
+
private contentKey;
|
|
38
|
+
getAnchor(roomId: string, channelId: string): Promise<AnyAnchor | null>;
|
|
39
|
+
setAnchor(roomId: string, channelId: string, anchor: AnyAnchor): Promise<void>;
|
|
40
|
+
deleteAnchor(roomId: string, channelId: string): Promise<void>;
|
|
41
|
+
listAnchors(roomId: string): Promise<Record<string, AnyAnchor | null>>;
|
|
42
|
+
getMeta(roomId: string): Promise<Record<string, unknown>>;
|
|
43
|
+
setMeta(roomId: string, meta: Record<string, unknown>): Promise<void>;
|
|
44
|
+
getContentData(roomId: string): Promise<Record<string, unknown> | null>;
|
|
45
|
+
setContentData(roomId: string, data: Record<string, unknown>): Promise<void>;
|
|
46
|
+
}
|
|
47
|
+
interface RedisTransportOptions {
|
|
48
|
+
/** Client used to PUBLISH snapshots. */
|
|
49
|
+
redis: Redis;
|
|
50
|
+
/**
|
|
51
|
+
* Factory for a fresh subscriber connection. A Redis connection in subscribe
|
|
52
|
+
* mode can't issue other commands, so each `subscribe()` gets its own.
|
|
53
|
+
*/
|
|
54
|
+
createSubscriber: () => Redis;
|
|
55
|
+
/** Channel namespace prefix. Default `"syncframe"`. */
|
|
56
|
+
prefix?: string;
|
|
57
|
+
}
|
|
58
|
+
declare class RedisTransport implements SyncTransport {
|
|
59
|
+
private readonly redis;
|
|
60
|
+
private readonly createSubscriber;
|
|
61
|
+
private readonly prefix;
|
|
62
|
+
constructor(options: RedisTransportOptions);
|
|
63
|
+
private channel;
|
|
64
|
+
publish(roomId: string, snapshot: CoreSnapshot): Promise<void>;
|
|
65
|
+
subscribe(roomId: string, handler: (snapshot: CoreSnapshot) => void): Promise<() => void>;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export { RedisStore, type RedisStoreOptions, RedisTransport, type RedisTransportOptions };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
var DEFAULT_PREFIX = "syncframe";
|
|
3
|
+
async function readJson(redis, key) {
|
|
4
|
+
const raw = await redis.get(key);
|
|
5
|
+
return raw ? JSON.parse(raw) : null;
|
|
6
|
+
}
|
|
7
|
+
var RedisStore = class {
|
|
8
|
+
redis;
|
|
9
|
+
prefix;
|
|
10
|
+
constructor(options) {
|
|
11
|
+
this.redis = options.redis;
|
|
12
|
+
this.prefix = options.prefix ?? DEFAULT_PREFIX;
|
|
13
|
+
}
|
|
14
|
+
ns(roomId) {
|
|
15
|
+
return `${this.prefix}:${roomId}`;
|
|
16
|
+
}
|
|
17
|
+
anchorKey(roomId, channelId) {
|
|
18
|
+
return `${this.ns(roomId)}:anchor:${channelId}`;
|
|
19
|
+
}
|
|
20
|
+
channelsKey(roomId) {
|
|
21
|
+
return `${this.ns(roomId)}:channels`;
|
|
22
|
+
}
|
|
23
|
+
metaKey(roomId) {
|
|
24
|
+
return `${this.ns(roomId)}:meta`;
|
|
25
|
+
}
|
|
26
|
+
contentKey(roomId) {
|
|
27
|
+
return `${this.ns(roomId)}:content`;
|
|
28
|
+
}
|
|
29
|
+
async getAnchor(roomId, channelId) {
|
|
30
|
+
return readJson(this.redis, this.anchorKey(roomId, channelId));
|
|
31
|
+
}
|
|
32
|
+
async setAnchor(roomId, channelId, anchor) {
|
|
33
|
+
await Promise.all([
|
|
34
|
+
this.redis.set(this.anchorKey(roomId, channelId), JSON.stringify(anchor)),
|
|
35
|
+
this.redis.sadd(this.channelsKey(roomId), channelId)
|
|
36
|
+
]);
|
|
37
|
+
}
|
|
38
|
+
async deleteAnchor(roomId, channelId) {
|
|
39
|
+
await Promise.all([
|
|
40
|
+
this.redis.del(this.anchorKey(roomId, channelId)),
|
|
41
|
+
this.redis.srem(this.channelsKey(roomId), channelId)
|
|
42
|
+
]);
|
|
43
|
+
}
|
|
44
|
+
async listAnchors(roomId) {
|
|
45
|
+
const channels = await this.redis.smembers(this.channelsKey(roomId));
|
|
46
|
+
const entries = await Promise.all(
|
|
47
|
+
channels.map(async (channelId) => [
|
|
48
|
+
channelId,
|
|
49
|
+
await this.getAnchor(roomId, channelId)
|
|
50
|
+
])
|
|
51
|
+
);
|
|
52
|
+
return Object.fromEntries(entries);
|
|
53
|
+
}
|
|
54
|
+
async getMeta(roomId) {
|
|
55
|
+
return await readJson(this.redis, this.metaKey(roomId)) ?? {};
|
|
56
|
+
}
|
|
57
|
+
async setMeta(roomId, meta) {
|
|
58
|
+
await this.redis.set(this.metaKey(roomId), JSON.stringify(meta));
|
|
59
|
+
}
|
|
60
|
+
async getContentData(roomId) {
|
|
61
|
+
return readJson(this.redis, this.contentKey(roomId));
|
|
62
|
+
}
|
|
63
|
+
async setContentData(roomId, data) {
|
|
64
|
+
await this.redis.set(this.contentKey(roomId), JSON.stringify(data));
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
var RedisTransport = class {
|
|
68
|
+
redis;
|
|
69
|
+
createSubscriber;
|
|
70
|
+
prefix;
|
|
71
|
+
constructor(options) {
|
|
72
|
+
this.redis = options.redis;
|
|
73
|
+
this.createSubscriber = options.createSubscriber;
|
|
74
|
+
this.prefix = options.prefix ?? DEFAULT_PREFIX;
|
|
75
|
+
}
|
|
76
|
+
channel(roomId) {
|
|
77
|
+
return `${this.prefix}:${roomId}:updates`;
|
|
78
|
+
}
|
|
79
|
+
async publish(roomId, snapshot) {
|
|
80
|
+
await this.redis.publish(this.channel(roomId), JSON.stringify(snapshot));
|
|
81
|
+
}
|
|
82
|
+
async subscribe(roomId, handler) {
|
|
83
|
+
const subscriber = this.createSubscriber();
|
|
84
|
+
const channel = this.channel(roomId);
|
|
85
|
+
subscriber.on("message", (incoming, message) => {
|
|
86
|
+
if (incoming === channel) handler(JSON.parse(message));
|
|
87
|
+
});
|
|
88
|
+
await subscriber.subscribe(channel);
|
|
89
|
+
return () => {
|
|
90
|
+
void subscriber.quit();
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
export { RedisStore, RedisTransport };
|
|
96
|
+
//# sourceMappingURL=index.js.map
|
|
97
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * @syncframe/redis — Redis-backed adapters for @syncframe/core.\n *\n * Implements core's `SyncStore` and `SyncTransport` interfaces on top of Redis,\n * so a `SyncServer` can persist anchors and fan out snapshots across processes\n * and serverless instances.\n *\n * Core stays zero-dependency: these adapters only *implement* its interfaces.\n * Every `@syncframe/core` and `ioredis` import here is type-only and erased at\n * compile time — the package has no runtime dependency on core, and the Redis\n * client is injected by the caller (so connection, auth, and pooling concerns\n * live in the application, not here).\n */\n\nimport type Redis from 'ioredis';\n// Type-only imports from core's server entry. That entry is React-free, so it\n// never pulls hooks (which reference DOM globals) into this non-DOM package's\n// type-check.\nimport type {\n SyncStore,\n SyncTransport,\n AnyAnchor,\n CoreSnapshot,\n} from '@syncframe/core/server';\n\nconst DEFAULT_PREFIX = 'syncframe';\n\nasync function readJson<T>(redis: Redis, key: string): Promise<T | null> {\n const raw = await redis.get(key);\n return raw ? (JSON.parse(raw) as T) : null;\n}\n\nexport interface RedisStoreOptions {\n /** Client used for all commands. */\n redis: Redis;\n /** Key namespace prefix. Default `\"syncframe\"`. */\n prefix?: string;\n}\n\n/**\n * Anchors, meta, and content data are stored as JSON under namespaced keys.\n * The set of channel ids per room is tracked in a Redis set so `listAnchors`\n * (and therefore snapshot building) works without key scanning.\n */\nexport class RedisStore implements SyncStore {\n private readonly redis: Redis;\n private readonly prefix: string;\n\n constructor(options: RedisStoreOptions) {\n this.redis = options.redis;\n this.prefix = options.prefix ?? DEFAULT_PREFIX;\n }\n\n private ns(roomId: string): string {\n return `${this.prefix}:${roomId}`;\n }\n private anchorKey(roomId: string, channelId: string): string {\n return `${this.ns(roomId)}:anchor:${channelId}`;\n }\n private channelsKey(roomId: string): string {\n return `${this.ns(roomId)}:channels`;\n }\n private metaKey(roomId: string): string {\n return `${this.ns(roomId)}:meta`;\n }\n private contentKey(roomId: string): string {\n return `${this.ns(roomId)}:content`;\n }\n\n async getAnchor(roomId: string, channelId: string): Promise<AnyAnchor | null> {\n return readJson<AnyAnchor>(this.redis, this.anchorKey(roomId, channelId));\n }\n\n async setAnchor(roomId: string, channelId: string, anchor: AnyAnchor): Promise<void> {\n await Promise.all([\n this.redis.set(this.anchorKey(roomId, channelId), JSON.stringify(anchor)),\n this.redis.sadd(this.channelsKey(roomId), channelId),\n ]);\n }\n\n async deleteAnchor(roomId: string, channelId: string): Promise<void> {\n await Promise.all([\n this.redis.del(this.anchorKey(roomId, channelId)),\n this.redis.srem(this.channelsKey(roomId), channelId),\n ]);\n }\n\n async listAnchors(roomId: string): Promise<Record<string, AnyAnchor | null>> {\n const channels = await this.redis.smembers(this.channelsKey(roomId));\n const entries = await Promise.all(\n channels.map(async (channelId): Promise<[string, AnyAnchor | null]> => [\n channelId,\n await this.getAnchor(roomId, channelId),\n ]),\n );\n return Object.fromEntries(entries);\n }\n\n async getMeta(roomId: string): Promise<Record<string, unknown>> {\n return (await readJson<Record<string, unknown>>(this.redis, this.metaKey(roomId))) ?? {};\n }\n\n async setMeta(roomId: string, meta: Record<string, unknown>): Promise<void> {\n await this.redis.set(this.metaKey(roomId), JSON.stringify(meta));\n }\n\n async getContentData(roomId: string): Promise<Record<string, unknown> | null> {\n return readJson<Record<string, unknown>>(this.redis, this.contentKey(roomId));\n }\n\n async setContentData(roomId: string, data: Record<string, unknown>): Promise<void> {\n await this.redis.set(this.contentKey(roomId), JSON.stringify(data));\n }\n}\n\nexport interface RedisTransportOptions {\n /** Client used to PUBLISH snapshots. */\n redis: Redis;\n /**\n * Factory for a fresh subscriber connection. A Redis connection in subscribe\n * mode can't issue other commands, so each `subscribe()` gets its own.\n */\n createSubscriber: () => Redis;\n /** Channel namespace prefix. Default `\"syncframe\"`. */\n prefix?: string;\n}\n\nexport class RedisTransport implements SyncTransport {\n private readonly redis: Redis;\n private readonly createSubscriber: () => Redis;\n private readonly prefix: string;\n\n constructor(options: RedisTransportOptions) {\n this.redis = options.redis;\n this.createSubscriber = options.createSubscriber;\n this.prefix = options.prefix ?? DEFAULT_PREFIX;\n }\n\n private channel(roomId: string): string {\n return `${this.prefix}:${roomId}:updates`;\n }\n\n async publish(roomId: string, snapshot: CoreSnapshot): Promise<void> {\n await this.redis.publish(this.channel(roomId), JSON.stringify(snapshot));\n }\n\n async subscribe(\n roomId: string,\n handler: (snapshot: CoreSnapshot) => void,\n ): Promise<() => void> {\n const subscriber = this.createSubscriber();\n const channel = this.channel(roomId);\n\n subscriber.on('message', (incoming: string, message: string) => {\n if (incoming === channel) handler(JSON.parse(message) as CoreSnapshot);\n });\n await subscriber.subscribe(channel);\n\n return () => {\n void subscriber.quit();\n };\n }\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@syncframe/redis",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Redis-backed SyncStore + SyncTransport adapters for @syncframe/core",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"keywords": [
|
|
9
|
+
"syncframe",
|
|
10
|
+
"redis",
|
|
11
|
+
"ioredis",
|
|
12
|
+
"pubsub",
|
|
13
|
+
"sync",
|
|
14
|
+
"realtime"
|
|
15
|
+
],
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/jkvc/syncframe.git",
|
|
19
|
+
"directory": "packages/redis"
|
|
20
|
+
},
|
|
21
|
+
"homepage": "https://github.com/jkvc/syncframe/tree/main/packages/redis#readme",
|
|
22
|
+
"bugs": "https://github.com/jkvc/syncframe/issues",
|
|
23
|
+
"main": "./dist/index.js",
|
|
24
|
+
"types": "./dist/index.d.ts",
|
|
25
|
+
"exports": {
|
|
26
|
+
".": {
|
|
27
|
+
"types": "./dist/index.d.ts",
|
|
28
|
+
"import": "./dist/index.js"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"dist"
|
|
33
|
+
],
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@syncframe/core": "^0.1.0"
|
|
36
|
+
},
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"ioredis": "^5"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@types/node": "^20",
|
|
42
|
+
"ioredis": "^5.11.1",
|
|
43
|
+
"tsup": "^8.5.1",
|
|
44
|
+
"typescript": "^5"
|
|
45
|
+
},
|
|
46
|
+
"publishConfig": {
|
|
47
|
+
"access": "public"
|
|
48
|
+
},
|
|
49
|
+
"scripts": {
|
|
50
|
+
"build": "tsup",
|
|
51
|
+
"type-check": "tsc --noEmit",
|
|
52
|
+
"test": "vitest run"
|
|
53
|
+
}
|
|
54
|
+
}
|