@signe/room 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,189 @@
1
+ import { Request as Request$1, WebSocket, DurableObjectState, AnalyticsEngineDataset, DurableObjectStorage, VectorizeIndex, R2Bucket, KVNamespace } from '@cloudflare/workers-types';
2
+
3
+ declare function action(name: string, bodyValidation?: any): (target: any, propertyKey: string) => void;
4
+ declare function Room$1(options: any): (target: any) => void;
5
+
6
+ type AssetFetcher = {
7
+ fetch(path: string): Promise<Response | null>;
8
+ };
9
+ type StandardRequest = globalThis.Request;
10
+ interface Request extends Request$1 {
11
+ }
12
+ type ReturnRequest = StandardRequest | Request$1;
13
+ /** Per-party key-value storage */
14
+ interface Storage extends DurableObjectStorage {
15
+ }
16
+ /** Connection metadata only available when the connection is made */
17
+ type ConnectionContext = {
18
+ request: Request$1;
19
+ };
20
+ type Stub = {
21
+ /** @deprecated Use `await socket()` instead */
22
+ connect: () => WebSocket;
23
+ socket(pathOrInit?: string | RequestInit): Promise<WebSocket>;
24
+ socket(path: string, init?: RequestInit): Promise<WebSocket>;
25
+ fetch(pathOrInit?: string | RequestInit | ReturnRequest): Promise<Response>;
26
+ fetch(path: string, init?: RequestInit | ReturnRequest): Promise<Response>;
27
+ };
28
+ /** Additional information about other resources in the current project */
29
+ type Context = {
30
+ /** Access other parties in this project */
31
+ parties: Record<string, {
32
+ get(id: string): Stub;
33
+ }>;
34
+ /**
35
+ * A binding to the Cloudflare AI service.
36
+ */
37
+ ai: AI;
38
+ /**
39
+ * A binding to the Cloudflare Vectorize service.
40
+ */
41
+ vectorize: Record<string, VectorizeIndex>;
42
+ /**
43
+ * A binding to fetch static assets
44
+ */
45
+ assets: AssetFetcher;
46
+ /**
47
+ * Custom bindings
48
+ */
49
+ bindings: CustomBindings;
50
+ };
51
+ type AI = Record<string, never>;
52
+ type ImmutablePrimitive = undefined | null | boolean | string | number;
53
+ type Immutable<T> = T extends ImmutablePrimitive ? T : T extends Array<infer U> ? ImmutableArray<U> : T extends Map<infer K, infer V> ? ImmutableMap<K, V> : T extends Set<infer M> ? ImmutableSet<M> : ImmutableObject<T>;
54
+ type ImmutableArray<T> = ReadonlyArray<Immutable<T>>;
55
+ type ImmutableMap<K, V> = ReadonlyMap<Immutable<K>, Immutable<V>>;
56
+ type ImmutableSet<T> = ReadonlySet<Immutable<T>>;
57
+ type ImmutableObject<T> = {
58
+ readonly [K in keyof T]: Immutable<T[K]>;
59
+ };
60
+ type ConnectionState<T> = ImmutableObject<T> | null;
61
+ type ConnectionSetStateFn<T> = (prevState: ConnectionState<T>) => T;
62
+ /** A WebSocket connected to the Room */
63
+ type Connection<TState = unknown> = WebSocket & {
64
+ /** Connection identifier */
65
+ id: string;
66
+ /** @deprecated You can access the socket properties directly on the connection*/
67
+ socket: WebSocket;
68
+ uri: string;
69
+ /**
70
+ * Arbitrary state associated with this connection.
71
+ * Read-only, use Connection.setState to update the state.
72
+ */
73
+ state: ConnectionState<TState>;
74
+ setState(state: TState | ConnectionSetStateFn<TState> | null): ConnectionState<TState>;
75
+ /** @deprecated use Connection.setState instead */
76
+ serializeAttachment<T = unknown>(attachment: T): void;
77
+ /** @deprecated use Connection.state instead */
78
+ deserializeAttachment<T = unknown>(): T | null;
79
+ };
80
+ type CustomBindings = {
81
+ r2: Record<string, R2Bucket>;
82
+ kv: Record<string, KVNamespace>;
83
+ };
84
+ /** Room represents a single, self-contained, long-lived session. */
85
+ type Room = {
86
+ /** Room ID defined in the Party URL, e.g. /parties/:name/:id */
87
+ id: string;
88
+ /** Internal ID assigned by the platform. Use Party.id instead. */
89
+ internalID: string;
90
+ /** Party name defined in the Party URL, e.g. /parties/:name/:id */
91
+ name: string;
92
+ /** Environment variables (--var, partykit.json#vars, or .env) */
93
+ env: Record<string, unknown>;
94
+ /** A per-room key-value storage */
95
+ storage: Storage;
96
+ /** `blockConcurrencyWhile()` ensures no requests are delivered until */
97
+ blockConcurrencyWhile: DurableObjectState["blockConcurrencyWhile"];
98
+ /** Additional information about other resources in the current project */
99
+ context: Context;
100
+ /** @deprecated Use `room.getConnections` instead */
101
+ connections: Map<string, Connection>;
102
+ /** @deprecated Use `room.context.parties` instead */
103
+ parties: Context["parties"];
104
+ /** Send a message to all connected clients, except connection ids listed `without` */
105
+ broadcast: (msg: string | ArrayBuffer | ArrayBufferView, without?: string[] | undefined) => void;
106
+ /** Get a connection by connection id */
107
+ getConnection<TState = unknown>(id: string): Connection<TState> | undefined;
108
+ /**
109
+ * Get all connections. Optionally, you can provide a tag to filter returned connections.
110
+ * Use `Party.Server#getConnectionTags` to tag the connection on connect.
111
+ */
112
+ getConnections<TState = unknown>(tag?: string): Iterable<Connection<TState>>;
113
+ /**
114
+ * Cloudflare Analytics Engine dataset. Use this to log custom events and metrics.
115
+ */
116
+ analytics: AnalyticsEngineDataset;
117
+ };
118
+ type Server$1 = {
119
+ /**
120
+ * You can define an `options` field to customise the Party.Server behaviour.
121
+ */
122
+ readonly options?: ServerOptions;
123
+ /**
124
+ * You can tag a connection to filter them in Party#getConnections.
125
+ * Each connection supports up to 9 tags, each tag max length is 256 characters.
126
+ */
127
+ getConnectionTags?(connection: Connection, context: ConnectionContext): string[] | Promise<string[]>;
128
+ /**
129
+ * Called when the server is started, before first `onConnect` or `onRequest`.
130
+ * Useful for loading data from storage.
131
+ *
132
+ * You can use this to load data from storage and perform other asynchronous
133
+ * initialization, such as retrieving data or configuration from other
134
+ * services or databases.
135
+ */
136
+ onStart?(): void | Promise<void>;
137
+ /**
138
+ * Called when a new incoming WebSocket connection is opened.
139
+ */
140
+ onConnect?(connection: Connection, ctx: ConnectionContext): void | Promise<void>;
141
+ /**
142
+ * Called when a WebSocket connection receives a message from a client, or another connected party.
143
+ */
144
+ onMessage?(message: string | ArrayBuffer | ArrayBufferView, sender: Connection): void | Promise<void>;
145
+ /**
146
+ * Called when a WebSocket connection is closed by the client.
147
+ */
148
+ onClose?(connection: Connection): void | Promise<void>;
149
+ /**
150
+ * Called when a WebSocket connection is closed due to a connection error.
151
+ */
152
+ onError?(connection: Connection, error: Error): void | Promise<void>;
153
+ /**
154
+ * Called when a HTTP request is made to the room URL.
155
+ */
156
+ onRequest?(req: Request): Response | Promise<Response>;
157
+ /**
158
+ * Called when an alarm is triggered. Use Party.storage.setAlarm to set an alarm.
159
+ *
160
+ * Alarms have access to most Party resources such as storage, but not Party.id
161
+ * and Party.context.parties properties. Attempting to access them will result in a
162
+ * runtime error.
163
+ */
164
+ onAlarm?(): void | Promise<void>;
165
+ };
166
+ type ServerOptions = {
167
+ /**
168
+ * Whether the PartyKit platform should remove the server from memory
169
+ * between HTTP requests and WebSocket messages.
170
+ *
171
+ * The default value is `false`.
172
+ */
173
+ hibernate?: boolean;
174
+ };
175
+
176
+ declare class Server implements Server$1 {
177
+ readonly room: Room;
178
+ memoryAll: {};
179
+ subRoom: {};
180
+ rooms: any[];
181
+ static onBeforeConnect(request: Request): Promise<Response | Request>;
182
+ constructor(room: Room);
183
+ private getUsersProperty;
184
+ onConnect(conn: Connection, ctx: ConnectionContext): Promise<void>;
185
+ onMessage(message: string, sender: Connection): Promise<void>;
186
+ onClose(conn: Connection): Promise<void>;
187
+ }
188
+
189
+ export { Room$1 as Room, Server, action };
package/dist/index.js ADDED
@@ -0,0 +1,226 @@
1
+ // src/decorators.ts
2
+ function action(name, bodyValidation) {
3
+ return function(target, propertyKey) {
4
+ if (!target.constructor._actionMetadata) {
5
+ target.constructor._actionMetadata = /* @__PURE__ */ new Map();
6
+ }
7
+ target.constructor._actionMetadata.set(name, {
8
+ key: propertyKey,
9
+ bodyValidation
10
+ });
11
+ };
12
+ }
13
+ function Room(options) {
14
+ return function(target) {
15
+ target.path = options.path;
16
+ target.maxUsers = options.maxUsers;
17
+ target.throttleStorage = options.throttleStorage;
18
+ };
19
+ }
20
+
21
+ // src/server.ts
22
+ import { createStatesSnapshot, getByPath, load, syncClass } from "@signe/sync";
23
+ import { dset as dset2 } from "dset";
24
+ import z from "zod";
25
+
26
+ // src/utils.ts
27
+ import { dset } from "dset";
28
+ function isPromise(value) {
29
+ return value && value instanceof Promise;
30
+ }
31
+ async function awaitReturn(val) {
32
+ return isPromise(val) ? await val : val;
33
+ }
34
+ function isClass(obj) {
35
+ return typeof obj === "function" && obj.prototype && obj.prototype.constructor === obj;
36
+ }
37
+ function throttle(func, wait) {
38
+ let timeout = null;
39
+ let lastArgs = null;
40
+ return function(...args) {
41
+ if (!timeout) {
42
+ func(...args);
43
+ timeout = setTimeout(() => {
44
+ if (lastArgs) {
45
+ func(...lastArgs);
46
+ lastArgs = null;
47
+ }
48
+ timeout = null;
49
+ }, wait);
50
+ } else {
51
+ lastArgs = args;
52
+ }
53
+ };
54
+ }
55
+ function extractParams(pattern, str) {
56
+ const regexPattern = pattern.replace(/{(\w+)}/g, "(?<$1>[\\w-]+)");
57
+ const regex = new RegExp(`^${regexPattern}$`);
58
+ const match = regex.exec(str);
59
+ if (match && match.groups) {
60
+ return match.groups;
61
+ } else {
62
+ return null;
63
+ }
64
+ }
65
+ function dremove(obj, keys) {
66
+ keys.split && (keys = keys.split("."));
67
+ var i = 0, l = keys.length, t = { ...obj }, k;
68
+ while (i < l - 1) {
69
+ k = keys[i++];
70
+ if (k === "__proto__" || k === "constructor" || k === "prototype") return;
71
+ t = t[k];
72
+ if (typeof t !== "object" || t === null) return;
73
+ }
74
+ k = keys[i];
75
+ if (t && typeof t === "object" && !(k === "__proto__" || k === "constructor" || k === "prototype")) {
76
+ delete t[k];
77
+ }
78
+ }
79
+ function buildObject(valuesMap, allMemory) {
80
+ let memoryObj = {};
81
+ for (let path of valuesMap.keys()) {
82
+ const value = valuesMap.get(path);
83
+ dset(memoryObj, path, value);
84
+ if (path == "$delete") {
85
+ dremove(allMemory, value);
86
+ } else {
87
+ dset(allMemory, path, value);
88
+ }
89
+ }
90
+ return memoryObj;
91
+ }
92
+
93
+ // src/server.ts
94
+ var Message = z.object({
95
+ action: z.string(),
96
+ value: z.any()
97
+ });
98
+ var Server = class {
99
+ constructor(room) {
100
+ this.room = room;
101
+ this.memoryAll = {};
102
+ this.subRoom = {};
103
+ this.rooms = [];
104
+ for (let room2 of this.rooms) {
105
+ const params = extractParams(room2.path, this.room.id);
106
+ if (params) {
107
+ this.subRoom = new room2(this.room, params);
108
+ break;
109
+ }
110
+ }
111
+ if (!this.subRoom) {
112
+ throw new Error("Room not found");
113
+ }
114
+ const loadMemory = async () => {
115
+ const root = await this.room.storage.get(".");
116
+ const memory = await this.room.storage.list();
117
+ const tmpObject = root || {};
118
+ for (let [key, value] of memory) {
119
+ if (key == ".") {
120
+ continue;
121
+ }
122
+ dset2(tmpObject, key, value);
123
+ }
124
+ load(this, tmpObject);
125
+ };
126
+ loadMemory();
127
+ syncClass(this.subRoom, {
128
+ onSync: throttle((values) => {
129
+ const packet = buildObject(values, this.memoryAll);
130
+ this.room.broadcast(
131
+ JSON.stringify({
132
+ type: "sync",
133
+ value: packet
134
+ })
135
+ );
136
+ values.clear();
137
+ }, 500),
138
+ onPersist: throttle(async (values) => {
139
+ for (let path of values) {
140
+ const instance = path == "." ? this.subRoom : getByPath(this.subRoom, path);
141
+ const itemValue = createStatesSnapshot(instance);
142
+ await this.room.storage.put(path, itemValue);
143
+ }
144
+ values.clear();
145
+ }, this.subRoom["throttleStorage"] ?? 2e3)
146
+ });
147
+ }
148
+ static async onBeforeConnect(request) {
149
+ try {
150
+ request.headers.set("X-User-ID", "" + Math.random());
151
+ return request;
152
+ } catch (e) {
153
+ return new Response("Unauthorized", { status: 401 });
154
+ }
155
+ }
156
+ getUsersProperty() {
157
+ const meta = this.subRoom.constructor["_propertyMetadata"];
158
+ const propId = meta?.get("users");
159
+ if (propId) {
160
+ return this.subRoom[propId];
161
+ }
162
+ return null;
163
+ }
164
+ async onConnect(conn, ctx) {
165
+ const publicId = "a" + ("" + Math.random()).split(".")[1];
166
+ let user = null;
167
+ const signal = this.getUsersProperty();
168
+ if (signal) {
169
+ const { classType } = signal.options;
170
+ user = isClass(classType) ? new classType() : classType(conn, ctx);
171
+ signal()[publicId] = user;
172
+ }
173
+ await awaitReturn(this.subRoom["onJoin"]?.(user, conn, ctx));
174
+ conn.setState({ publicId });
175
+ conn.send(
176
+ JSON.stringify({
177
+ type: "sync",
178
+ value: {
179
+ pId: publicId,
180
+ ...this.memoryAll
181
+ }
182
+ })
183
+ );
184
+ }
185
+ async onMessage(message, sender) {
186
+ const actions = this.subRoom.constructor["_actionMetadata"];
187
+ const result = Message.safeParse(JSON.parse(message));
188
+ if (!result.success) {
189
+ return;
190
+ }
191
+ if (actions) {
192
+ const signal = this.getUsersProperty();
193
+ const { publicId } = sender.state;
194
+ const user = signal?.()[publicId];
195
+ const actionName = actions.get(result.data.action);
196
+ if (actionName) {
197
+ if (actionName.bodyValidation) {
198
+ const bodyResult = actionName.bodyValidation.safeParse(
199
+ result.data.value
200
+ );
201
+ if (!bodyResult.success) {
202
+ return;
203
+ }
204
+ }
205
+ await awaitReturn(
206
+ this.subRoom[actionName.key](user, result.data.value, sender)
207
+ );
208
+ }
209
+ }
210
+ }
211
+ async onClose(conn) {
212
+ const signal = this.getUsersProperty();
213
+ const { publicId } = conn.state;
214
+ const user = signal?.()[publicId];
215
+ await awaitReturn(this.subRoom["onLeave"]?.(user, conn));
216
+ if (signal) {
217
+ delete signal()[publicId];
218
+ }
219
+ }
220
+ };
221
+ export {
222
+ Room,
223
+ Server,
224
+ action
225
+ };
226
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/decorators.ts","../src/server.ts","../src/utils.ts"],"sourcesContent":["export function action(name: string, bodyValidation?) {\n return function (target: any, propertyKey: string) {\n if (!target.constructor._actionMetadata) {\n target.constructor._actionMetadata = new Map();\n }\n target.constructor._actionMetadata.set(name, {\n key: propertyKey,\n bodyValidation,\n });\n };\n}\n\nexport function Room(options) {\n return function (target: any) {\n target.path = options.path;\n target.maxUsers = options.maxUsers;\n target.throttleStorage = options.throttleStorage;\n };\n}\n","import { createStatesSnapshot, getByPath, load, syncClass } from \"@signe/sync\";\nimport { dset } from \"dset\";\nimport z from \"zod\";\nimport type * as Party from \"./types/party\";\nimport {\n awaitReturn,\n buildObject,\n extractParams,\n isClass,\n throttle,\n} from \"./utils\";\n\nconst Message = z.object({\n action: z.string(),\n value: z.any(),\n});\n\nexport class Server implements Party.Server {\n memoryAll = {};\n subRoom = {};\n rooms = [];\n\n static async onBeforeConnect(request: Party.Request) {\n try {\n request.headers.set(\"X-User-ID\", \"\" + Math.random());\n return request;\n } catch (e) {\n return new Response(\"Unauthorized\", { status: 401 });\n }\n }\n\n constructor(readonly room: Party.Room) {\n for (let room of this.rooms) {\n const params = extractParams(room.path, this.room.id);\n if (params) {\n this.subRoom = new room(this.room, params);\n break;\n }\n }\n\n if (!this.subRoom) {\n throw new Error(\"Room not found\");\n }\n\n const loadMemory = async () => {\n const root = await this.room.storage.get(\".\");\n const memory = await this.room.storage.list();\n const tmpObject: any = root || {};\n for (let [key, value] of memory) {\n if (key == \".\") {\n continue;\n }\n dset(tmpObject, key, value);\n }\n load(this, tmpObject);\n };\n\n loadMemory();\n\n syncClass(this.subRoom, {\n onSync: throttle((values) => {\n const packet = buildObject(values, this.memoryAll);\n this.room.broadcast(\n JSON.stringify({\n type: \"sync\",\n value: packet,\n })\n );\n values.clear();\n }, 500),\n onPersist: throttle(async (values) => {\n for (let path of values) {\n const instance =\n path == \".\" ? this.subRoom : getByPath(this.subRoom, path);\n const itemValue = createStatesSnapshot(instance);\n await this.room.storage.put(path, itemValue);\n }\n values.clear();\n }, this.subRoom['throttleStorage'] ?? 2000),\n });\n }\n\n private getUsersProperty() {\n const meta = this.subRoom.constructor['_propertyMetadata'];\n const propId = meta?.get(\"users\");\n if (propId) {\n return this.subRoom[propId];\n }\n return null;\n }\n\n async onConnect(conn: Party.Connection, ctx: Party.ConnectionContext) {\n const publicId = \"a\" + (\"\" + Math.random()).split(\".\")[1];\n let user = null;\n const signal = this.getUsersProperty();\n if (signal) {\n const { classType } = signal.options;\n user = isClass(classType) ? new classType() : classType(conn, ctx);\n signal()[publicId] = user;\n }\n await awaitReturn(this.subRoom['onJoin']?.(user, conn, ctx));\n conn.setState({ publicId });\n conn.send(\n JSON.stringify({\n type: \"sync\",\n value: {\n pId: publicId,\n ...this.memoryAll,\n },\n })\n );\n }\n\n async onMessage(message: string, sender: Party.Connection) {\n const actions = this.subRoom.constructor['_actionMetadata'];\n const result = Message.safeParse(JSON.parse(message));\n if (!result.success) {\n return;\n }\n if (actions) {\n const signal = this.getUsersProperty();\n const { publicId } = sender.state as any;\n const user = signal?.()[publicId];\n const actionName = actions.get(result.data.action);\n if (actionName) {\n if (actionName.bodyValidation) {\n const bodyResult = actionName.bodyValidation.safeParse(\n result.data.value\n );\n if (!bodyResult.success) {\n return;\n }\n }\n await awaitReturn(\n this.subRoom[actionName.key](user, result.data.value, sender)\n );\n }\n }\n }\n\n async onClose(conn: Party.Connection) {\n const signal = this.getUsersProperty();\n const { publicId } = conn.state as any;\n const user = signal?.()[publicId];\n await awaitReturn(this.subRoom['onLeave']?.(user, conn));\n if (signal) {\n delete signal()[publicId];\n }\n }\n}\n","import { dset } from \"dset\";\n\nexport function isPromise(value: any): boolean {\n return value && value instanceof Promise;\n}\n\nexport async function awaitReturn(val: any) {\n return isPromise(val) ? await val : val;\n}\n\nexport function isClass(obj: any): boolean {\n return (\n typeof obj === \"function\" &&\n obj.prototype &&\n obj.prototype.constructor === obj\n );\n}\n\nexport function throttle<F extends (...args: any[]) => any>(\n func: F,\n wait: number\n): (...args: Parameters<F>) => void {\n let timeout: ReturnType<typeof setTimeout> | null = null;\n let lastArgs: Parameters<F> | null = null;\n\n return function (...args: Parameters<F>) {\n if (!timeout) {\n func(...args);\n timeout = setTimeout(() => {\n if (lastArgs) {\n func(...lastArgs);\n lastArgs = null;\n }\n timeout = null;\n }, wait);\n } else {\n lastArgs = args;\n }\n };\n}\n\nexport function extractParams(\n pattern: string,\n str: string\n): { [key: string]: string } | null {\n const regexPattern = pattern.replace(/{(\\w+)}/g, \"(?<$1>[\\\\w-]+)\");\n\n const regex = new RegExp(`^${regexPattern}$`);\n const match = regex.exec(str);\n\n if (match && match.groups) {\n return match.groups;\n } else {\n return null;\n }\n}\n\nexport function dremove(obj, keys) {\n keys.split && (keys = keys.split(\".\"));\n var i = 0,\n l = keys.length,\n t = { ...obj },\n k;\n\n while (i < l - 1) {\n k = keys[i++];\n if (k === \"__proto__\" || k === \"constructor\" || k === \"prototype\") return; // On évite les clés dangereuses\n t = t[k];\n if (typeof t !== \"object\" || t === null) return; // Si l'objet n'existe pas, on arrête\n }\n\n k = keys[i];\n if (\n t &&\n typeof t === \"object\" &&\n !(k === \"__proto__\" || k === \"constructor\" || k === \"prototype\")\n ) {\n delete t[k];\n }\n}\n\nexport function buildObject(valuesMap, allMemory) {\n let memoryObj = {};\n for (let path of valuesMap.keys()) {\n const value = valuesMap.get(path);\n dset(memoryObj, path, value);\n if (path == \"$delete\") {\n dremove(allMemory, value);\n } else {\n dset(allMemory, path, value);\n }\n }\n return memoryObj;\n }"],"mappings":"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","names":["dset","room","dset"]}
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@signe/room",
3
+ "version": "0.0.1",
4
+ "description": "",
5
+ "main": "./dist/index.js",
6
+ "keywords": [],
7
+ "author": "Samuel Ronce",
8
+ "type": "module",
9
+ "exports": {
10
+ ".": {
11
+ "import": "./dist/index.js",
12
+ "types": "./dist/index.d.ts"
13
+ }
14
+ },
15
+ "license": "MIT",
16
+ "dependencies": {
17
+ "dset": "^3.1.3",
18
+ "partysocket": "^1.0.1",
19
+ "zod": "^3.23.8",
20
+ "@signe/sync": "0.0.1"
21
+ },
22
+ "publishConfig": {
23
+ "access": "public"
24
+ },
25
+ "devDependencies": {
26
+ "@cloudflare/workers-types": "^4.20240603.0"
27
+ },
28
+ "scripts": {
29
+ "build": "tsup src/index.ts",
30
+ "dev": "tsup src/index.ts --watch"
31
+ }
32
+ }
package/readme.md ADDED
@@ -0,0 +1,54 @@
1
+ # Example
2
+
3
+ ```ts
4
+ import { signal } from "@signe/reactive";
5
+ import { Room, Server, action } from "@signe/room";
6
+ import { id, sync, users } from "@signe/sync";
7
+
8
+ export class Player {
9
+ @id() id = signal("");
10
+ @sync() x = signal(0);
11
+ @sync() y = signal(0);
12
+ @sync() color = signal("#000000");
13
+
14
+ constructor() {
15
+ const randomColor = Math.floor(Math.random() * 16777215).toString(16);
16
+ this.color.set("#" + randomColor);
17
+ }
18
+ }
19
+
20
+ export class RoomSchema {
21
+ @sync() count = signal(0);
22
+ @users(Player) players = signal({});
23
+ }
24
+
25
+ @Room({
26
+ path: "chess-{id}",
27
+ maxUsers: 2,
28
+ })
29
+ export class MyRoom extends RoomSchema {
30
+ static onAuth() {}
31
+
32
+ constructor(readonly room, readonly params: { id: string }) {
33
+ super();
34
+ }
35
+
36
+ onCreate() {}
37
+
38
+ @action("move")
39
+ move(player: Player, data: any) {
40
+ player.x.set(data.x);
41
+ player.y.set(data.y);
42
+ }
43
+
44
+ onJoin(player: Player) {
45
+ console.log(player.id(), "joined");
46
+ }
47
+
48
+ onLeave() {}
49
+ }
50
+
51
+ export default class MyServer extends Server {
52
+ rooms = [MyRoom];
53
+ }
54
+ ```
@@ -0,0 +1,19 @@
1
+ export function action(name: string, bodyValidation?) {
2
+ return function (target: any, propertyKey: string) {
3
+ if (!target.constructor._actionMetadata) {
4
+ target.constructor._actionMetadata = new Map();
5
+ }
6
+ target.constructor._actionMetadata.set(name, {
7
+ key: propertyKey,
8
+ bodyValidation,
9
+ });
10
+ };
11
+ }
12
+
13
+ export function Room(options) {
14
+ return function (target: any) {
15
+ target.path = options.path;
16
+ target.maxUsers = options.maxUsers;
17
+ target.throttleStorage = options.throttleStorage;
18
+ };
19
+ }
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export * from './decorators';
2
+ export { Server } from './server';
3
+
package/src/server.ts ADDED
@@ -0,0 +1,150 @@
1
+ import { createStatesSnapshot, getByPath, load, syncClass } from "@signe/sync";
2
+ import { dset } from "dset";
3
+ import z from "zod";
4
+ import type * as Party from "./types/party";
5
+ import {
6
+ awaitReturn,
7
+ buildObject,
8
+ extractParams,
9
+ isClass,
10
+ throttle,
11
+ } from "./utils";
12
+
13
+ const Message = z.object({
14
+ action: z.string(),
15
+ value: z.any(),
16
+ });
17
+
18
+ export class Server implements Party.Server {
19
+ memoryAll = {};
20
+ subRoom = {};
21
+ rooms = [];
22
+
23
+ static async onBeforeConnect(request: Party.Request) {
24
+ try {
25
+ request.headers.set("X-User-ID", "" + Math.random());
26
+ return request;
27
+ } catch (e) {
28
+ return new Response("Unauthorized", { status: 401 });
29
+ }
30
+ }
31
+
32
+ constructor(readonly room: Party.Room) {
33
+ for (let room of this.rooms) {
34
+ const params = extractParams(room.path, this.room.id);
35
+ if (params) {
36
+ this.subRoom = new room(this.room, params);
37
+ break;
38
+ }
39
+ }
40
+
41
+ if (!this.subRoom) {
42
+ throw new Error("Room not found");
43
+ }
44
+
45
+ const loadMemory = async () => {
46
+ const root = await this.room.storage.get(".");
47
+ const memory = await this.room.storage.list();
48
+ const tmpObject: any = root || {};
49
+ for (let [key, value] of memory) {
50
+ if (key == ".") {
51
+ continue;
52
+ }
53
+ dset(tmpObject, key, value);
54
+ }
55
+ load(this, tmpObject);
56
+ };
57
+
58
+ loadMemory();
59
+
60
+ syncClass(this.subRoom, {
61
+ onSync: throttle((values) => {
62
+ const packet = buildObject(values, this.memoryAll);
63
+ this.room.broadcast(
64
+ JSON.stringify({
65
+ type: "sync",
66
+ value: packet,
67
+ })
68
+ );
69
+ values.clear();
70
+ }, 500),
71
+ onPersist: throttle(async (values) => {
72
+ for (let path of values) {
73
+ const instance =
74
+ path == "." ? this.subRoom : getByPath(this.subRoom, path);
75
+ const itemValue = createStatesSnapshot(instance);
76
+ await this.room.storage.put(path, itemValue);
77
+ }
78
+ values.clear();
79
+ }, this.subRoom['throttleStorage'] ?? 2000),
80
+ });
81
+ }
82
+
83
+ private getUsersProperty() {
84
+ const meta = this.subRoom.constructor['_propertyMetadata'];
85
+ const propId = meta?.get("users");
86
+ if (propId) {
87
+ return this.subRoom[propId];
88
+ }
89
+ return null;
90
+ }
91
+
92
+ async onConnect(conn: Party.Connection, ctx: Party.ConnectionContext) {
93
+ const publicId = "a" + ("" + Math.random()).split(".")[1];
94
+ let user = null;
95
+ const signal = this.getUsersProperty();
96
+ if (signal) {
97
+ const { classType } = signal.options;
98
+ user = isClass(classType) ? new classType() : classType(conn, ctx);
99
+ signal()[publicId] = user;
100
+ }
101
+ await awaitReturn(this.subRoom['onJoin']?.(user, conn, ctx));
102
+ conn.setState({ publicId });
103
+ conn.send(
104
+ JSON.stringify({
105
+ type: "sync",
106
+ value: {
107
+ pId: publicId,
108
+ ...this.memoryAll,
109
+ },
110
+ })
111
+ );
112
+ }
113
+
114
+ async onMessage(message: string, sender: Party.Connection) {
115
+ const actions = this.subRoom.constructor['_actionMetadata'];
116
+ const result = Message.safeParse(JSON.parse(message));
117
+ if (!result.success) {
118
+ return;
119
+ }
120
+ if (actions) {
121
+ const signal = this.getUsersProperty();
122
+ const { publicId } = sender.state as any;
123
+ const user = signal?.()[publicId];
124
+ const actionName = actions.get(result.data.action);
125
+ if (actionName) {
126
+ if (actionName.bodyValidation) {
127
+ const bodyResult = actionName.bodyValidation.safeParse(
128
+ result.data.value
129
+ );
130
+ if (!bodyResult.success) {
131
+ return;
132
+ }
133
+ }
134
+ await awaitReturn(
135
+ this.subRoom[actionName.key](user, result.data.value, sender)
136
+ );
137
+ }
138
+ }
139
+ }
140
+
141
+ async onClose(conn: Party.Connection) {
142
+ const signal = this.getUsersProperty();
143
+ const { publicId } = conn.state as any;
144
+ const user = signal?.()[publicId];
145
+ await awaitReturn(this.subRoom['onLeave']?.(user, conn));
146
+ if (signal) {
147
+ delete signal()[publicId];
148
+ }
149
+ }
150
+ }
package/src/storage.ts ADDED
@@ -0,0 +1,15 @@
1
+ export class Storage {
2
+ private memory = new Map();
3
+ async put(key, value) {
4
+ this.memory.set(key, value);
5
+ }
6
+ async get(key) {
7
+ return this.memory.get(key);
8
+ }
9
+ async delete(key) {
10
+ this.memory.delete(key);
11
+ }
12
+ async list() {
13
+ return this.memory;
14
+ }
15
+ }
@@ -0,0 +1,531 @@
1
+ import type {
2
+ AnalyticsEngineDataset,
3
+ ExecutionContext as CFExecutionContext,
4
+ Request as CFRequest,
5
+ DurableObjectState,
6
+ DurableObjectStorage,
7
+ KVNamespace,
8
+ R2Bucket,
9
+ ScheduledController,
10
+ VectorizeIndex,
11
+ WebSocket
12
+ } from "@cloudflare/workers-types";
13
+
14
+ export type StaticAssetsManifestType = {
15
+ devServer: string;
16
+ browserTTL: number | null | undefined;
17
+ edgeTTL: number | null | undefined;
18
+ singlePageApp: boolean | undefined;
19
+ assets: Record<string, string>;
20
+ assetInfo?: Record<
21
+ string,
22
+ {
23
+ fileSize: number;
24
+ fileHash: string;
25
+ fileName: string;
26
+ }
27
+ >;
28
+ };
29
+
30
+ type AssetFetcher = {
31
+ fetch(path: string): Promise<Response | null>;
32
+ };
33
+
34
+ type StandardRequest = globalThis.Request;
35
+
36
+ // Types with PartyKit* prefix are used in module workers, i.e.
37
+ // `export default {} satisfies PartyKitServer;`
38
+
39
+ // Types with Party.* prefix are used in class workers, i.e.
40
+ // `export default class Room implements Party.Server {}`
41
+
42
+ // Extend type so that when language server (e.g. vscode) autocompletes,
43
+ // it will import this type instead of the underlying type directly.
44
+ export interface Request extends CFRequest {}
45
+
46
+ // Because when you construct a `new Request()` in a user script,
47
+ // it's assumed to be a standards-based Fetch API Response, unless overridden.
48
+ // This is fine by us, let user return whichever request type
49
+ type ReturnRequest = StandardRequest | CFRequest;
50
+
51
+ /** Per-party key-value storage */
52
+ export interface Storage extends DurableObjectStorage {}
53
+
54
+ /** Connection metadata only available when the connection is made */
55
+ export type ConnectionContext = { request: CFRequest };
56
+
57
+ export type Stub = {
58
+ /** @deprecated Use `await socket()` instead */
59
+ connect: () => WebSocket;
60
+ socket(pathOrInit?: string | RequestInit): Promise<WebSocket>;
61
+ socket(path: string, init?: RequestInit): Promise<WebSocket>;
62
+ fetch(pathOrInit?: string | RequestInit | ReturnRequest): Promise<Response>;
63
+ fetch(path: string, init?: RequestInit | ReturnRequest): Promise<Response>;
64
+ };
65
+
66
+ /** Additional information about other resources in the current project */
67
+ export type Context = {
68
+ /** Access other parties in this project */
69
+ parties: Record<
70
+ string,
71
+ {
72
+ get(id: string): Stub;
73
+ }
74
+ >;
75
+ /**
76
+ * A binding to the Cloudflare AI service.
77
+ */
78
+ ai: AI;
79
+ /**
80
+ * A binding to the Cloudflare Vectorize service.
81
+ */
82
+ vectorize: Record<string, VectorizeIndex>;
83
+
84
+ /**
85
+ * A binding to fetch static assets
86
+ */
87
+ assets: AssetFetcher;
88
+
89
+ /**
90
+ * Custom bindings
91
+ */
92
+ bindings: CustomBindings;
93
+ };
94
+
95
+ export type AI = Record<string, never>;
96
+
97
+ export type FetchLobby = {
98
+ env: Record<string, unknown>;
99
+ ai: AI;
100
+ parties: Context["parties"];
101
+ vectorize: Context["vectorize"];
102
+ analytics: AnalyticsEngineDataset;
103
+ assets: AssetFetcher;
104
+ bindings: CustomBindings;
105
+ };
106
+
107
+ export type CronLobby = {
108
+ env: Record<string, unknown>;
109
+ ai: AI;
110
+ parties: Context["parties"];
111
+ vectorize: Context["vectorize"];
112
+ analytics: AnalyticsEngineDataset;
113
+ assets: AssetFetcher;
114
+ bindings: CustomBindings;
115
+ };
116
+
117
+ export type Lobby = {
118
+ id: string;
119
+ env: Record<string, unknown>;
120
+ ai: AI;
121
+ parties: Context["parties"];
122
+ vectorize: Context["vectorize"];
123
+ analytics: AnalyticsEngineDataset;
124
+ assets: AssetFetcher;
125
+ bindings: CustomBindings;
126
+ };
127
+
128
+ export type ExecutionContext = CFExecutionContext;
129
+
130
+ // https://stackoverflow.com/a/58993872
131
+ type ImmutablePrimitive = undefined | null | boolean | string | number;
132
+ type Immutable<T> = T extends ImmutablePrimitive
133
+ ? T
134
+ : T extends Array<infer U>
135
+ ? ImmutableArray<U>
136
+ : T extends Map<infer K, infer V>
137
+ ? ImmutableMap<K, V>
138
+ : T extends Set<infer M>
139
+ ? ImmutableSet<M>
140
+ : ImmutableObject<T>;
141
+ type ImmutableArray<T> = ReadonlyArray<Immutable<T>>;
142
+ type ImmutableMap<K, V> = ReadonlyMap<Immutable<K>, Immutable<V>>;
143
+ type ImmutableSet<T> = ReadonlySet<Immutable<T>>;
144
+ type ImmutableObject<T> = { readonly [K in keyof T]: Immutable<T[K]> };
145
+
146
+ export type ConnectionState<T> = ImmutableObject<T> | null;
147
+ export type ConnectionSetStateFn<T> = (prevState: ConnectionState<T>) => T;
148
+
149
+ /** A WebSocket connected to the Room */
150
+ export type Connection<TState = unknown> = WebSocket & {
151
+ /** Connection identifier */
152
+ id: string;
153
+
154
+ /** @deprecated You can access the socket properties directly on the connection*/
155
+ socket: WebSocket;
156
+ // We would have been able to use Websocket::url
157
+ // but it's not available in the Workers runtime
158
+ // (rather, url is `null` when using WebSocketPair)
159
+ // It's also set as readonly, so we can't set it ourselves.
160
+ // Instead, we'll use the `uri` property.
161
+ uri: string;
162
+
163
+ /**
164
+ * Arbitrary state associated with this connection.
165
+ * Read-only, use Connection.setState to update the state.
166
+ */
167
+ state: ConnectionState<TState>;
168
+
169
+ setState(
170
+ state: TState | ConnectionSetStateFn<TState> | null
171
+ ): ConnectionState<TState>;
172
+
173
+ /** @deprecated use Connection.setState instead */
174
+ serializeAttachment<T = unknown>(attachment: T): void;
175
+
176
+ /** @deprecated use Connection.state instead */
177
+ deserializeAttachment<T = unknown>(): T | null;
178
+ };
179
+
180
+ type CustomBindings = {
181
+ r2: Record<string, R2Bucket>;
182
+ kv: Record<string, KVNamespace>;
183
+ };
184
+
185
+ /** Room represents a single, self-contained, long-lived session. */
186
+ export type Room = {
187
+ /** Room ID defined in the Party URL, e.g. /parties/:name/:id */
188
+ id: string;
189
+
190
+ /** Internal ID assigned by the platform. Use Party.id instead. */
191
+ internalID: string;
192
+
193
+ /** Party name defined in the Party URL, e.g. /parties/:name/:id */
194
+ name: string;
195
+
196
+ /** Environment variables (--var, partykit.json#vars, or .env) */
197
+ env: Record<string, unknown>;
198
+
199
+ /** A per-room key-value storage */
200
+ storage: Storage;
201
+
202
+ /** `blockConcurrencyWhile()` ensures no requests are delivered until */
203
+ blockConcurrencyWhile: DurableObjectState["blockConcurrencyWhile"];
204
+
205
+ /** Additional information about other resources in the current project */
206
+ context: Context;
207
+
208
+ /** @deprecated Use `room.getConnections` instead */
209
+ connections: Map<string, Connection>;
210
+
211
+ /** @deprecated Use `room.context.parties` instead */
212
+ parties: Context["parties"];
213
+
214
+ /** Send a message to all connected clients, except connection ids listed `without` */
215
+ broadcast: (
216
+ msg: string | ArrayBuffer | ArrayBufferView,
217
+ without?: string[] | undefined
218
+ ) => void;
219
+
220
+ /** Get a connection by connection id */
221
+ getConnection<TState = unknown>(id: string): Connection<TState> | undefined;
222
+
223
+ /**
224
+ * Get all connections. Optionally, you can provide a tag to filter returned connections.
225
+ * Use `Party.Server#getConnectionTags` to tag the connection on connect.
226
+ */
227
+ getConnections<TState = unknown>(tag?: string): Iterable<Connection<TState>>;
228
+
229
+ /**
230
+ * Cloudflare Analytics Engine dataset. Use this to log custom events and metrics.
231
+ */
232
+ analytics: AnalyticsEngineDataset;
233
+ };
234
+
235
+ /** @deprecated Use `Party.Room` instead */
236
+ export type Party = Room;
237
+
238
+ /* Party.Server defines what happens when someone connects to and sends messages or HTTP requests to your party
239
+ *
240
+ * @example
241
+ * export default class Room implements Party.Server {
242
+ * constructor(readonly room: Party) {}
243
+ * onConnect(connection: Party.Connection) {
244
+ * this.room.broadcast("Someone connected with id " + connection.id);
245
+ * }
246
+ * }
247
+ */
248
+ export type Server = {
249
+ /**
250
+ * You can define an `options` field to customise the Party.Server behaviour.
251
+ */
252
+ readonly options?: ServerOptions;
253
+
254
+ /**
255
+ * You can tag a connection to filter them in Party#getConnections.
256
+ * Each connection supports up to 9 tags, each tag max length is 256 characters.
257
+ */
258
+ getConnectionTags?(
259
+ connection: Connection,
260
+ context: ConnectionContext
261
+ ): string[] | Promise<string[]>;
262
+
263
+ /**
264
+ * Called when the server is started, before first `onConnect` or `onRequest`.
265
+ * Useful for loading data from storage.
266
+ *
267
+ * You can use this to load data from storage and perform other asynchronous
268
+ * initialization, such as retrieving data or configuration from other
269
+ * services or databases.
270
+ */
271
+ onStart?(): void | Promise<void>;
272
+
273
+ /**
274
+ * Called when a new incoming WebSocket connection is opened.
275
+ */
276
+ onConnect?(
277
+ connection: Connection,
278
+ ctx: ConnectionContext
279
+ ): void | Promise<void>;
280
+
281
+ /**
282
+ * Called when a WebSocket connection receives a message from a client, or another connected party.
283
+ */
284
+ onMessage?(
285
+ message: string | ArrayBuffer | ArrayBufferView,
286
+ sender: Connection
287
+ ): void | Promise<void>;
288
+
289
+ /**
290
+ * Called when a WebSocket connection is closed by the client.
291
+ */
292
+ onClose?(connection: Connection): void | Promise<void>;
293
+
294
+ /**
295
+ * Called when a WebSocket connection is closed due to a connection error.
296
+ */
297
+ onError?(connection: Connection, error: Error): void | Promise<void>;
298
+
299
+ /**
300
+ * Called when a HTTP request is made to the room URL.
301
+ */
302
+ onRequest?(req: Request): Response | Promise<Response>;
303
+
304
+ /**
305
+ * Called when an alarm is triggered. Use Party.storage.setAlarm to set an alarm.
306
+ *
307
+ * Alarms have access to most Party resources such as storage, but not Party.id
308
+ * and Party.context.parties properties. Attempting to access them will result in a
309
+ * runtime error.
310
+ */
311
+ onAlarm?(): void | Promise<void>;
312
+ };
313
+
314
+ export type FetchSocket = WebSocket & {
315
+ request: Request;
316
+ };
317
+
318
+ export type Cron = ScheduledController & {
319
+ name: string;
320
+ };
321
+
322
+ type ServerConstructor = {
323
+ new (room: Room): Server;
324
+ };
325
+
326
+ /**
327
+ * Party.Worker allows you to customise the behaviour of the Edge worker that routes
328
+ * connections to your party.
329
+ *
330
+ * The Party.Worker methods can be defined as static methods on the Party.Server constructor.
331
+ * @example
332
+ * export default class Room implements Party.Server {
333
+ * static onBeforeConnect(req: Party.Request) {
334
+ * return new Response("Access denied", { status: 403 })
335
+ * }
336
+ * constructor(readonly room: Room) {}
337
+ * }
338
+ *
339
+ * Room satisfies Party.Worker;
340
+ */
341
+
342
+ export type Worker = ServerConstructor & {
343
+ /**
344
+ * Runs on any HTTP request that does not match a Party URL or a static asset.
345
+ * Useful for running lightweight HTTP endpoints that don't need access to the Party
346
+ * state.
347
+ **/
348
+ onFetch?(
349
+ req: Request,
350
+ lobby: FetchLobby,
351
+ ctx: ExecutionContext
352
+ ): Response | undefined | null | Promise<Response | null | undefined>;
353
+
354
+ /**
355
+ * Runs on any WebSocket connection that does not match a Party URL or a static asset.
356
+ * Useful for running lightweight WebSocket endpoints that don't need access to the Party
357
+ * state.
358
+ */
359
+ onSocket?(
360
+ socket: FetchSocket,
361
+ lobby: FetchLobby,
362
+ ctx: ExecutionContext
363
+ ): void | Promise<void>;
364
+
365
+ /**
366
+ * Runs before any HTTP request is made to the party. You can modify the request
367
+ * before it is forwarded to the party, or return a Response to short-circuit it.
368
+ */
369
+ onBeforeRequest?(
370
+ req: Request,
371
+ lobby: Lobby,
372
+ ctx: ExecutionContext
373
+ ): Request | Response | Promise<Request | Response>;
374
+
375
+ /**
376
+ * Runs before any WebSocket connection is made to the party. You can modify the request
377
+ * before opening a connection, or return a Response to prevent the connection.
378
+ */
379
+ onBeforeConnect?(
380
+ req: Request,
381
+ lobby: Lobby,
382
+ ctx: ExecutionContext
383
+ ): Request | Response | Promise<Request | Response>;
384
+
385
+ /**
386
+ * Runs on a schedule. You can use this to perform periodic tasks, such as
387
+ * sending a heartbeat to a third-party service.
388
+ */
389
+ onCron?(
390
+ controller: Cron,
391
+ lobby: CronLobby,
392
+ ctx: ExecutionContext
393
+ ): Response | Promise<Response>;
394
+ };
395
+
396
+ /**
397
+ * PartyKitServer is allows you to customise the behaviour of your Party.
398
+ *
399
+ * @note If you're starting a new project, we recommend using the newer
400
+ * Party.Server API instead.
401
+ *
402
+ * @example
403
+ * export default {
404
+ * onConnect(connection, room) {
405
+ * room.broadcast("Someone connected with id " + connection.id);
406
+ * }
407
+ * }
408
+ */
409
+ export type PartyKitServer = {
410
+ /** @deprecated Use `onFetch` instead */
411
+ unstable_onFetch?: (
412
+ req: Request,
413
+ lobby: FetchLobby,
414
+ ctx: ExecutionContext
415
+ ) => Response | null | undefined | Promise<Response | null | undefined>;
416
+ onFetch?: (
417
+ req: Request,
418
+ lobby: FetchLobby,
419
+ ctx: ExecutionContext
420
+ ) => Response | null | undefined | Promise<Response | null | undefined>;
421
+ onSocket?(
422
+ socket: FetchSocket,
423
+ lobby: FetchLobby,
424
+ ctx: ExecutionContext
425
+ ): void | Promise<void>;
426
+ onBeforeRequest?: (
427
+ req: Request,
428
+ lobby: Lobby,
429
+ ctx: ExecutionContext
430
+ ) => ReturnRequest | Response | Promise<ReturnRequest | Response>;
431
+
432
+ onCron?: (
433
+ controller: Cron,
434
+ lobby: CronLobby,
435
+ ctx: ExecutionContext
436
+ ) => void | Promise<void>;
437
+
438
+ onRequest?: (req: Request, room: Room) => Response | Promise<Response>;
439
+ onAlarm?: (room: Omit<Room, "id" | "parties">) => void | Promise<void>;
440
+ onConnect?: (
441
+ connection: Connection,
442
+ room: Room,
443
+ ctx: ConnectionContext
444
+ ) => void | Promise<void>;
445
+ onBeforeConnect?: (
446
+ req: Request,
447
+ lobby: Lobby,
448
+ ctx: ExecutionContext
449
+ ) => ReturnRequest | Response | Promise<ReturnRequest | Response>;
450
+
451
+ /**
452
+ * PartyKitServer may opt into being hibernated between WebSocket
453
+ * messages, which enables a single server to handle more connections.
454
+ */
455
+ onMessage?: (
456
+ message: string | ArrayBuffer | ArrayBufferView,
457
+ connection: Connection,
458
+ room: Room
459
+ ) => void | Promise<void>;
460
+ onClose?: (connection: Connection, room: Room) => void | Promise<void>;
461
+ onError?: (
462
+ connection: Connection,
463
+ err: Error,
464
+ room: Room
465
+ ) => void | Promise<void>;
466
+ };
467
+
468
+ export type ServerOptions = {
469
+ /**
470
+ * Whether the PartyKit platform should remove the server from memory
471
+ * between HTTP requests and WebSocket messages.
472
+ *
473
+ * The default value is `false`.
474
+ */
475
+ hibernate?: boolean;
476
+ };
477
+
478
+ //
479
+ // ---
480
+ // DEPRECATIONS
481
+ // ---
482
+ //
483
+
484
+ /** @deprecated Use Party.Request instead */
485
+ export type PartyRequest = Request;
486
+
487
+ /** @deprecated Use Party.Storage instead */
488
+ export type PartyStorage = Storage;
489
+
490
+ /** @deprecated Use Party.Storage instead */
491
+ export type PartyKitStorage = Storage;
492
+
493
+ /** @deprecated Use Party.ConnectionContext instead */
494
+ export type PartyConnectionContext = ConnectionContext;
495
+
496
+ /** @deprecated Use Party.ConnectionContext instead */
497
+ export type PartyKitContext = ConnectionContext;
498
+
499
+ /** @deprecated Use Party.Stub instead */
500
+ export type PartyStub = Stub;
501
+
502
+ /** Additional information about other resources in the current project */
503
+ /** @deprecated Use Party.Context instead */
504
+ export type PartyContext = Context;
505
+
506
+ /** @deprecated Use Party.FetchLobby instead */
507
+ export type PartyFetchLobby = FetchLobby;
508
+
509
+ /** @deprecated Use Party.Lobby instead */
510
+ export type PartyLobby = Lobby;
511
+
512
+ /** @deprecated Use Party.ExecutionContext instead */
513
+ export type PartyExecutionContext = ExecutionContext;
514
+
515
+ /** @deprecated Use Party.Connection instead */
516
+ export type PartyConnection = Connection;
517
+
518
+ /** @deprecated Use Party.Server instead */
519
+ export type PartyServer = Server;
520
+
521
+ /** @deprecated Use Party.Worker instead */
522
+ export type PartyWorker = Worker;
523
+
524
+ /** @deprecated Use `Room` instead */
525
+ export type PartyKitRoom = Room;
526
+
527
+ /** @deprecated Use `Party.Connection` instead */
528
+ export type PartyKitConnection = Connection;
529
+
530
+ /** @deprecated Use `Party.ServerOptions` instead */
531
+ export type PartyServerOptions = ServerOptions;
package/src/utils.ts ADDED
@@ -0,0 +1,94 @@
1
+ import { dset } from "dset";
2
+
3
+ export function isPromise(value: any): boolean {
4
+ return value && value instanceof Promise;
5
+ }
6
+
7
+ export async function awaitReturn(val: any) {
8
+ return isPromise(val) ? await val : val;
9
+ }
10
+
11
+ export function isClass(obj: any): boolean {
12
+ return (
13
+ typeof obj === "function" &&
14
+ obj.prototype &&
15
+ obj.prototype.constructor === obj
16
+ );
17
+ }
18
+
19
+ export function throttle<F extends (...args: any[]) => any>(
20
+ func: F,
21
+ wait: number
22
+ ): (...args: Parameters<F>) => void {
23
+ let timeout: ReturnType<typeof setTimeout> | null = null;
24
+ let lastArgs: Parameters<F> | null = null;
25
+
26
+ return function (...args: Parameters<F>) {
27
+ if (!timeout) {
28
+ func(...args);
29
+ timeout = setTimeout(() => {
30
+ if (lastArgs) {
31
+ func(...lastArgs);
32
+ lastArgs = null;
33
+ }
34
+ timeout = null;
35
+ }, wait);
36
+ } else {
37
+ lastArgs = args;
38
+ }
39
+ };
40
+ }
41
+
42
+ export function extractParams(
43
+ pattern: string,
44
+ str: string
45
+ ): { [key: string]: string } | null {
46
+ const regexPattern = pattern.replace(/{(\w+)}/g, "(?<$1>[\\w-]+)");
47
+
48
+ const regex = new RegExp(`^${regexPattern}$`);
49
+ const match = regex.exec(str);
50
+
51
+ if (match && match.groups) {
52
+ return match.groups;
53
+ } else {
54
+ return null;
55
+ }
56
+ }
57
+
58
+ export function dremove(obj, keys) {
59
+ keys.split && (keys = keys.split("."));
60
+ var i = 0,
61
+ l = keys.length,
62
+ t = { ...obj },
63
+ k;
64
+
65
+ while (i < l - 1) {
66
+ k = keys[i++];
67
+ if (k === "__proto__" || k === "constructor" || k === "prototype") return; // On évite les clés dangereuses
68
+ t = t[k];
69
+ if (typeof t !== "object" || t === null) return; // Si l'objet n'existe pas, on arrête
70
+ }
71
+
72
+ k = keys[i];
73
+ if (
74
+ t &&
75
+ typeof t === "object" &&
76
+ !(k === "__proto__" || k === "constructor" || k === "prototype")
77
+ ) {
78
+ delete t[k];
79
+ }
80
+ }
81
+
82
+ export function buildObject(valuesMap, allMemory) {
83
+ let memoryObj = {};
84
+ for (let path of valuesMap.keys()) {
85
+ const value = valuesMap.get(path);
86
+ dset(memoryObj, path, value);
87
+ if (path == "$delete") {
88
+ dremove(allMemory, value);
89
+ } else {
90
+ dset(allMemory, path, value);
91
+ }
92
+ }
93
+ return memoryObj;
94
+ }