@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.
- package/dist/index.d.ts +189 -0
- package/dist/index.js +226 -0
- package/dist/index.js.map +1 -0
- package/package.json +32 -0
- package/readme.md +54 -0
- package/src/decorators.ts +19 -0
- package/src/index.ts +3 -0
- package/src/server.ts +150 -0
- package/src/storage.ts +15 -0
- package/src/types/party.ts +531 -0
- package/src/utils.ts +94 -0
package/dist/index.d.ts
ADDED
|
@@ -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
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
|
+
}
|