@signe/room 0.0.1 → 0.0.2
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 +195 -6
- package/dist/index.js +468 -57
- package/dist/index.js.map +1 -1
- package/examples/game/.vscode/launch.json +11 -0
- package/examples/game/.vscode/settings.json +11 -0
- package/examples/game/README.md +40 -0
- package/examples/game/app/client.tsx +40 -0
- package/examples/game/app/components/Counter.tsx +44 -0
- package/examples/game/app/styles.css +31 -0
- package/examples/game/package.json +20 -0
- package/examples/game/party/game.room.ts +12 -0
- package/examples/game/party/server.ts +12 -0
- package/examples/game/partykit.json +10 -0
- package/examples/game/public/favicon.ico +0 -0
- package/examples/game/public/index.html +27 -0
- package/examples/game/public/normalize.css +351 -0
- package/examples/game/shared/room.schema.ts +6 -0
- package/examples/game/tsconfig.json +109 -0
- package/package.json +2 -2
- package/src/decorators.ts +11 -2
- package/src/index.ts +1 -0
- package/src/mock.ts +70 -0
- package/src/server.ts +272 -48
- package/src/utils.ts +150 -24
package/src/server.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
|
-
import { createStatesSnapshot, getByPath, load, syncClass } from "@signe/sync";
|
|
2
1
|
import { dset } from "dset";
|
|
3
2
|
import z from "zod";
|
|
3
|
+
import {
|
|
4
|
+
createStatesSnapshot,
|
|
5
|
+
getByPath,
|
|
6
|
+
load,
|
|
7
|
+
syncClass,
|
|
8
|
+
} from "../../sync/src";
|
|
9
|
+
import { generateShortUUID } from "../../sync/src/utils";
|
|
4
10
|
import type * as Party from "./types/party";
|
|
5
11
|
import {
|
|
6
12
|
awaitReturn,
|
|
@@ -15,33 +21,121 @@ const Message = z.object({
|
|
|
15
21
|
value: z.any(),
|
|
16
22
|
});
|
|
17
23
|
|
|
24
|
+
type CreateRoomOptions = {
|
|
25
|
+
getMemoryAll?: boolean;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @class Server
|
|
30
|
+
* @implements {Party.Server}
|
|
31
|
+
* @description Represents a server that manages rooms and connections for a multiplayer game or application.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```typescript
|
|
35
|
+
* import { Room, Server, ServerIo } from "@yourpackage/room";
|
|
36
|
+
*
|
|
37
|
+
* @Room({ path: "game" })
|
|
38
|
+
* class GameRoom {
|
|
39
|
+
* // Room implementation
|
|
40
|
+
* }
|
|
41
|
+
*
|
|
42
|
+
* class MyServer extends Server {
|
|
43
|
+
* rooms = [GameRoom];
|
|
44
|
+
* }
|
|
45
|
+
*
|
|
46
|
+
* const server = new MyServer(new ServerIo("game"));
|
|
47
|
+
* server.onStart();
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
18
50
|
export class Server implements Party.Server {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
rooms = [];
|
|
51
|
+
subRoom = null;
|
|
52
|
+
rooms: any[] = [];
|
|
22
53
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
54
|
+
/**
|
|
55
|
+
* @constructor
|
|
56
|
+
* @param {Party.Room} room - The room object representing the current game or application instance.
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* ```typescript
|
|
60
|
+
* const server = new MyServer(new ServerIo("game"));
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
constructor(readonly room: Party.Room) {}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* @readonly
|
|
67
|
+
* @property {boolean} isHibernate - Indicates whether the server is in hibernate mode.
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* ```typescript
|
|
71
|
+
* if (!server.isHibernate) {
|
|
72
|
+
* console.log("Server is active");
|
|
73
|
+
* }
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
76
|
+
get isHibernate(): boolean {
|
|
77
|
+
return !!this["options"]?.hibernate;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* @method onStart
|
|
82
|
+
* @async
|
|
83
|
+
* @description Initializes the server and creates the initial room if not in hibernate mode.
|
|
84
|
+
* @returns {Promise<void>}
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* ```typescript
|
|
88
|
+
* async function initServer() {
|
|
89
|
+
* await server.onStart();
|
|
90
|
+
* console.log("Server started");
|
|
91
|
+
* }
|
|
92
|
+
* ```
|
|
93
|
+
*/
|
|
94
|
+
|
|
95
|
+
async onStart() {
|
|
96
|
+
// Only create a room if not in hibernate mode
|
|
97
|
+
// This prevents unnecessary resource allocation for inactive rooms
|
|
98
|
+
if (!this.isHibernate) {
|
|
99
|
+
this.subRoom = await this.createRoom();
|
|
29
100
|
}
|
|
30
101
|
}
|
|
31
102
|
|
|
32
|
-
|
|
103
|
+
/**
|
|
104
|
+
* @method createRoom
|
|
105
|
+
* @private
|
|
106
|
+
* @async
|
|
107
|
+
* @param {CreateRoomOptions} [options={}] - Options for creating the room.
|
|
108
|
+
* @returns {Promise<Object>} The created room instance.
|
|
109
|
+
* @throws {Error} If no matching room is found.
|
|
110
|
+
*
|
|
111
|
+
* @example
|
|
112
|
+
* ```typescript
|
|
113
|
+
* // This method is private and called internally
|
|
114
|
+
* async function internalCreateRoom() {
|
|
115
|
+
* const room = await this.createRoom({ getMemoryAll: true });
|
|
116
|
+
* console.log("Room created:", room);
|
|
117
|
+
* }
|
|
118
|
+
* ```
|
|
119
|
+
*/
|
|
120
|
+
private async createRoom(options: CreateRoomOptions = {}) {
|
|
121
|
+
let instance
|
|
122
|
+
let init = true
|
|
123
|
+
|
|
124
|
+
// Find the appropriate room based on the current room ID
|
|
33
125
|
for (let room of this.rooms) {
|
|
34
126
|
const params = extractParams(room.path, this.room.id);
|
|
35
127
|
if (params) {
|
|
36
|
-
|
|
128
|
+
instance = new room(this.room, params);
|
|
37
129
|
break;
|
|
38
130
|
}
|
|
39
131
|
}
|
|
40
132
|
|
|
41
|
-
if (!
|
|
133
|
+
if (!instance) {
|
|
42
134
|
throw new Error("Room not found");
|
|
43
135
|
}
|
|
44
136
|
|
|
137
|
+
// Load the room's memory from storage
|
|
138
|
+
// This ensures persistence across server restarts
|
|
45
139
|
const loadMemory = async () => {
|
|
46
140
|
const root = await this.room.storage.get(".");
|
|
47
141
|
const memory = await this.room.storage.list();
|
|
@@ -52,77 +146,188 @@ export class Server implements Party.Server {
|
|
|
52
146
|
}
|
|
53
147
|
dset(tmpObject, key, value);
|
|
54
148
|
}
|
|
55
|
-
load(
|
|
149
|
+
load(instance, tmpObject);
|
|
56
150
|
};
|
|
57
151
|
|
|
58
|
-
loadMemory();
|
|
152
|
+
await loadMemory();
|
|
59
153
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
154
|
+
instance.$memoryAll = {}
|
|
155
|
+
|
|
156
|
+
// Sync callback: Broadcast changes to all clients
|
|
157
|
+
const syncCb = (values) => {
|
|
158
|
+
if (options.getMemoryAll) {
|
|
159
|
+
buildObject(values, instance.$memoryAll);
|
|
160
|
+
}
|
|
161
|
+
if (init && this.isHibernate) {
|
|
162
|
+
init = false;
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
const packet = buildObject(values, instance.$memoryAll);
|
|
166
|
+
this.room.broadcast(
|
|
167
|
+
JSON.stringify({
|
|
168
|
+
type: "sync",
|
|
169
|
+
value: packet,
|
|
170
|
+
})
|
|
171
|
+
);
|
|
172
|
+
values.clear();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Persist callback: Save changes to storage
|
|
176
|
+
const persistCb = async (values) => {
|
|
177
|
+
for (let path of values) {
|
|
178
|
+
const _instance =
|
|
179
|
+
path == "." ? instance : getByPath(instance, path);
|
|
180
|
+
const itemValue = createStatesSnapshot(_instance);
|
|
181
|
+
await this.room.storage.put(path, itemValue);
|
|
182
|
+
}
|
|
183
|
+
values.clear();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Set up syncing and persistence with throttling to optimize performance
|
|
187
|
+
syncClass(instance, {
|
|
188
|
+
onSync: throttle(syncCb, instance["throttleSync"] ?? 500),
|
|
189
|
+
onPersist: throttle(persistCb, instance["throttleStorage"] ?? 2000),
|
|
80
190
|
});
|
|
191
|
+
|
|
192
|
+
return instance
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* @method getSubRoom
|
|
197
|
+
* @private
|
|
198
|
+
* @async
|
|
199
|
+
* @param {Object} [options={}] - Options for getting the sub-room.
|
|
200
|
+
* @returns {Promise<Object>} The sub-room instance.
|
|
201
|
+
*
|
|
202
|
+
* @example
|
|
203
|
+
* ```typescript
|
|
204
|
+
* // This method is private and called internally
|
|
205
|
+
* async function internalGetSubRoom() {
|
|
206
|
+
* const subRoom = await this.getSubRoom();
|
|
207
|
+
* console.log("Sub-room retrieved:", subRoom);
|
|
208
|
+
* }
|
|
209
|
+
* ```
|
|
210
|
+
*/
|
|
211
|
+
private async getSubRoom(options = {}) {
|
|
212
|
+
let subRoom
|
|
213
|
+
if (this.isHibernate) {
|
|
214
|
+
subRoom = await this.createRoom(options)
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
subRoom = this.subRoom
|
|
218
|
+
}
|
|
219
|
+
return subRoom
|
|
81
220
|
}
|
|
82
221
|
|
|
83
|
-
|
|
84
|
-
|
|
222
|
+
/**
|
|
223
|
+
* @method getUsersProperty
|
|
224
|
+
* @private
|
|
225
|
+
* @param {Object} subRoom - The sub-room instance.
|
|
226
|
+
* @returns {Object|null} The users property of the sub-room, or null if not found.
|
|
227
|
+
*
|
|
228
|
+
* @example
|
|
229
|
+
* ```typescript
|
|
230
|
+
* // This method is private and called internally
|
|
231
|
+
* function internalGetUsers(subRoom) {
|
|
232
|
+
* const users = this.getUsersProperty(subRoom);
|
|
233
|
+
* console.log("Users:", users);
|
|
234
|
+
* }
|
|
235
|
+
* ```
|
|
236
|
+
*/
|
|
237
|
+
|
|
238
|
+
private getUsersProperty(subRoom) {
|
|
239
|
+
const meta = subRoom.constructor["_propertyMetadata"];
|
|
85
240
|
const propId = meta?.get("users");
|
|
86
241
|
if (propId) {
|
|
87
|
-
return
|
|
242
|
+
return subRoom[propId];
|
|
88
243
|
}
|
|
89
244
|
return null;
|
|
90
245
|
}
|
|
91
246
|
|
|
247
|
+
/**
|
|
248
|
+
* @method onConnect
|
|
249
|
+
* @async
|
|
250
|
+
* @param {Party.Connection} conn - The connection object for the new user.
|
|
251
|
+
* @param {Party.ConnectionContext} ctx - The context of the connection.
|
|
252
|
+
* @description Handles a new user connection, creates a user object, and sends initial sync data.
|
|
253
|
+
* @returns {Promise<void>}
|
|
254
|
+
*
|
|
255
|
+
* @example
|
|
256
|
+
* ```typescript
|
|
257
|
+
* server.onConnect = async (conn, ctx) => {
|
|
258
|
+
* await server.onConnect(conn, ctx);
|
|
259
|
+
* console.log("New user connected:", conn.id);
|
|
260
|
+
* };
|
|
261
|
+
* ```
|
|
262
|
+
*/
|
|
92
263
|
async onConnect(conn: Party.Connection, ctx: Party.ConnectionContext) {
|
|
93
|
-
const
|
|
264
|
+
const subRoom = await this.getSubRoom({
|
|
265
|
+
getMemoryAll: true,
|
|
266
|
+
})
|
|
267
|
+
// Generate a unique public ID for the user
|
|
268
|
+
const publicId = generateShortUUID()
|
|
94
269
|
let user = null;
|
|
95
|
-
const signal = this.getUsersProperty();
|
|
270
|
+
const signal = this.getUsersProperty(subRoom);
|
|
96
271
|
if (signal) {
|
|
97
272
|
const { classType } = signal.options;
|
|
273
|
+
// Create a new user instance based on the defined class type
|
|
98
274
|
user = isClass(classType) ? new classType() : classType(conn, ctx);
|
|
99
275
|
signal()[publicId] = user;
|
|
100
276
|
}
|
|
101
|
-
|
|
277
|
+
// Call the room's onJoin method if it exists
|
|
278
|
+
await awaitReturn(subRoom["onJoin"]?.(user, conn, ctx));
|
|
102
279
|
conn.setState({ publicId });
|
|
280
|
+
// Send initial sync data to the new connection
|
|
103
281
|
conn.send(
|
|
104
282
|
JSON.stringify({
|
|
105
283
|
type: "sync",
|
|
106
284
|
value: {
|
|
107
285
|
pId: publicId,
|
|
108
|
-
...
|
|
286
|
+
...subRoom.$memoryAll,
|
|
109
287
|
},
|
|
110
288
|
})
|
|
111
289
|
);
|
|
112
290
|
}
|
|
113
291
|
|
|
292
|
+
/**
|
|
293
|
+
* @method onMessage
|
|
294
|
+
* @async
|
|
295
|
+
* @param {string} message - The message received from a user.
|
|
296
|
+
* @param {Party.Connection} sender - The connection object of the sender.
|
|
297
|
+
* @description Processes incoming messages and triggers corresponding actions in the sub-room.
|
|
298
|
+
* @returns {Promise<void>}
|
|
299
|
+
*
|
|
300
|
+
* @example
|
|
301
|
+
* ```typescript
|
|
302
|
+
* server.onMessage = async (message, sender) => {
|
|
303
|
+
* await server.onMessage(message, sender);
|
|
304
|
+
* console.log("Message processed from:", sender.id);
|
|
305
|
+
* };
|
|
306
|
+
* ```
|
|
307
|
+
*/
|
|
308
|
+
|
|
114
309
|
async onMessage(message: string, sender: Party.Connection) {
|
|
115
|
-
|
|
116
|
-
|
|
310
|
+
let json
|
|
311
|
+
try {
|
|
312
|
+
json = JSON.parse(message)
|
|
313
|
+
}
|
|
314
|
+
catch (e) {
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
// Validate incoming messages
|
|
318
|
+
const result = Message.safeParse(json);
|
|
117
319
|
if (!result.success) {
|
|
118
320
|
return;
|
|
119
321
|
}
|
|
322
|
+
const subRoom = await this.getSubRoom()
|
|
323
|
+
const actions = subRoom.constructor["_actionMetadata"];
|
|
120
324
|
if (actions) {
|
|
121
|
-
const signal = this.getUsersProperty();
|
|
325
|
+
const signal = this.getUsersProperty(subRoom);
|
|
122
326
|
const { publicId } = sender.state as any;
|
|
123
327
|
const user = signal?.()[publicId];
|
|
124
328
|
const actionName = actions.get(result.data.action);
|
|
125
329
|
if (actionName) {
|
|
330
|
+
// Validate action body if a validation schema is defined
|
|
126
331
|
if (actionName.bodyValidation) {
|
|
127
332
|
const bodyResult = actionName.bodyValidation.safeParse(
|
|
128
333
|
result.data.value
|
|
@@ -131,19 +336,38 @@ export class Server implements Party.Server {
|
|
|
131
336
|
return;
|
|
132
337
|
}
|
|
133
338
|
}
|
|
339
|
+
// Execute the action
|
|
134
340
|
await awaitReturn(
|
|
135
|
-
|
|
341
|
+
subRoom[actionName.key](user, result.data.value, sender)
|
|
136
342
|
);
|
|
137
343
|
}
|
|
138
344
|
}
|
|
139
345
|
}
|
|
140
346
|
|
|
347
|
+
/**
|
|
348
|
+
* @method onClose
|
|
349
|
+
* @async
|
|
350
|
+
* @param {Party.Connection} conn - The connection object of the disconnecting user.
|
|
351
|
+
* @description Handles user disconnection, removing them from the room and triggering the onLeave event.
|
|
352
|
+
* @returns {Promise<void>}
|
|
353
|
+
*
|
|
354
|
+
* @example
|
|
355
|
+
* ```typescript
|
|
356
|
+
* server.onClose = async (conn) => {
|
|
357
|
+
* await server.onClose(conn);
|
|
358
|
+
* console.log("User disconnected:", conn.id);
|
|
359
|
+
* };
|
|
360
|
+
* ```
|
|
361
|
+
*/
|
|
141
362
|
async onClose(conn: Party.Connection) {
|
|
142
|
-
const
|
|
363
|
+
const subRoom = await this.getSubRoom()
|
|
364
|
+
const signal = this.getUsersProperty(subRoom);
|
|
143
365
|
const { publicId } = conn.state as any;
|
|
144
366
|
const user = signal?.()[publicId];
|
|
145
|
-
|
|
367
|
+
// Call the room's onLeave method if it exists
|
|
368
|
+
await awaitReturn(subRoom["onLeave"]?.(user, conn));
|
|
146
369
|
if (signal) {
|
|
370
|
+
// Remove the user from the room
|
|
147
371
|
delete signal()[publicId];
|
|
148
372
|
}
|
|
149
373
|
}
|
package/src/utils.ts
CHANGED
|
@@ -1,14 +1,45 @@
|
|
|
1
1
|
import { dset } from "dset";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Checks if a value is a Promise.
|
|
5
|
+
*
|
|
6
|
+
* @param {unknown} value - The value to check.
|
|
7
|
+
* @returns {boolean} - Returns true if the value is a Promise, otherwise false.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* isPromise(Promise.resolve()); // true
|
|
11
|
+
* isPromise(42); // false
|
|
12
|
+
*/
|
|
13
|
+
export function isPromise(value: unknown): value is Promise<any> {
|
|
14
|
+
return value instanceof Promise;
|
|
5
15
|
}
|
|
6
16
|
|
|
7
|
-
|
|
17
|
+
/**
|
|
18
|
+
* Awaits the given value if it is a Promise, otherwise returns the value directly.
|
|
19
|
+
*
|
|
20
|
+
* @param {unknown} val - The value to await or return.
|
|
21
|
+
* @returns {Promise<any>} - Returns a Promise that resolves to the value.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* awaitReturn(Promise.resolve(42)); // 42
|
|
25
|
+
* awaitReturn(42); // 42
|
|
26
|
+
*/
|
|
27
|
+
export async function awaitReturn(val: unknown): Promise<any> {
|
|
8
28
|
return isPromise(val) ? await val : val;
|
|
9
29
|
}
|
|
10
30
|
|
|
11
|
-
|
|
31
|
+
/**
|
|
32
|
+
* Checks if a value is a class.
|
|
33
|
+
*
|
|
34
|
+
* @param {unknown} obj - The value to check.
|
|
35
|
+
* @returns {boolean} - Returns true if the value is a class, otherwise false.
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* class MyClass {}
|
|
39
|
+
* isClass(MyClass); // true
|
|
40
|
+
* isClass(() => {}); // false
|
|
41
|
+
*/
|
|
42
|
+
export function isClass(obj: unknown): boolean {
|
|
12
43
|
return (
|
|
13
44
|
typeof obj === "function" &&
|
|
14
45
|
obj.prototype &&
|
|
@@ -16,6 +47,24 @@ export function isClass(obj: any): boolean {
|
|
|
16
47
|
);
|
|
17
48
|
}
|
|
18
49
|
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Creates a throttled function that only invokes the provided function at most once per every wait milliseconds.
|
|
53
|
+
*
|
|
54
|
+
* The throttled function comes with a cancel method to cancel delayed invocations.
|
|
55
|
+
* If the throttled function is invoked more than once during the wait timeout,
|
|
56
|
+
* it will call the provided function with the latest arguments.
|
|
57
|
+
*
|
|
58
|
+
* @template F - The type of the function to throttle.
|
|
59
|
+
* @param {F} func - The function to throttle.
|
|
60
|
+
* @param {number} wait - The number of milliseconds to throttle invocations to.
|
|
61
|
+
* @returns {(...args: Parameters<F>) => void} - Returns the new throttled function.
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* const log = throttle((message) => console.log(message), 1000);
|
|
65
|
+
* log("Hello"); // Will log "Hello" immediately
|
|
66
|
+
* log("World"); // Will log "World" after 1 second, if no other calls to log() are made within the 1 second.
|
|
67
|
+
*/
|
|
19
68
|
export function throttle<F extends (...args: any[]) => any>(
|
|
20
69
|
func: F,
|
|
21
70
|
wait: number
|
|
@@ -39,34 +88,92 @@ export function throttle<F extends (...args: any[]) => any>(
|
|
|
39
88
|
};
|
|
40
89
|
}
|
|
41
90
|
|
|
91
|
+
/**
|
|
92
|
+
* Extracts parameters from a given string based on a specified pattern.
|
|
93
|
+
*
|
|
94
|
+
* The pattern can include placeholders in the form of {paramName}, which will be
|
|
95
|
+
* extracted from the input string if they match.
|
|
96
|
+
*
|
|
97
|
+
* @param {string} pattern - The pattern containing placeholders.
|
|
98
|
+
* @param {string} str - The string to extract parameters from.
|
|
99
|
+
* @returns {{ [key: string]: string } | null} - An object containing the extracted parameters,
|
|
100
|
+
* or null if the string does not match the pattern.
|
|
101
|
+
*
|
|
102
|
+
* @example
|
|
103
|
+
* // returns { id: '123' }
|
|
104
|
+
* extractParams('game-{id}', 'game-123');
|
|
105
|
+
*
|
|
106
|
+
* @example
|
|
107
|
+
* // returns { foo: 'abc', bar: 'xyz' }
|
|
108
|
+
* extractParams('test-{foo}-{bar}', 'test-abc-xyz');
|
|
109
|
+
*
|
|
110
|
+
*/
|
|
42
111
|
export function extractParams(
|
|
43
112
|
pattern: string,
|
|
44
113
|
str: string
|
|
45
114
|
): { [key: string]: string } | null {
|
|
115
|
+
// Replace placeholders in the pattern with named capture groups
|
|
46
116
|
const regexPattern = pattern.replace(/{(\w+)}/g, "(?<$1>[\\w-]+)");
|
|
47
117
|
|
|
118
|
+
// Create a strict regular expression from the pattern
|
|
48
119
|
const regex = new RegExp(`^${regexPattern}$`);
|
|
49
120
|
const match = regex.exec(str);
|
|
50
121
|
|
|
122
|
+
// If a match is found and groups are present, return the captured groups
|
|
51
123
|
if (match && match.groups) {
|
|
52
124
|
return match.groups;
|
|
125
|
+
} else if (pattern === str) {
|
|
126
|
+
// If the pattern exactly matches the string, return an empty object
|
|
127
|
+
return {};
|
|
53
128
|
} else {
|
|
129
|
+
// Otherwise, return null
|
|
54
130
|
return null;
|
|
55
131
|
}
|
|
56
132
|
}
|
|
57
133
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
134
|
+
/**
|
|
135
|
+
* Removes a property from an object based on a dot-separated key string or an array of keys.
|
|
136
|
+
*
|
|
137
|
+
* The function modifies the original object by deleting the specified property.
|
|
138
|
+
* It safely handles dangerous keys like __proto__, constructor, and prototype.
|
|
139
|
+
*
|
|
140
|
+
* @param {Record<string, any>} obj - The object from which to remove the property.
|
|
141
|
+
* @param {string | string[]} keys - The key(s) specifying the property to remove. Can be a dot-separated string or an array of strings.
|
|
142
|
+
*
|
|
143
|
+
* @example
|
|
144
|
+
* const obj = { a: { b: { c: 3 } } };
|
|
145
|
+
* dremove(obj, 'a.b.c');
|
|
146
|
+
* // obj is now { a: { b: {} } }
|
|
147
|
+
*
|
|
148
|
+
* @example
|
|
149
|
+
* const obj = { a: 1, b: 2 };
|
|
150
|
+
* dremove(obj, 'a');
|
|
151
|
+
* // obj is now { b: 2 }
|
|
152
|
+
*
|
|
153
|
+
* @example
|
|
154
|
+
* const obj = { a: { b: { c: 3 } } };
|
|
155
|
+
* dremove(obj, ['a', 'b', 'c']);
|
|
156
|
+
* // obj is now { a: { b: {} } }
|
|
157
|
+
*/
|
|
158
|
+
export function dremove(
|
|
159
|
+
obj: Record<string, any>,
|
|
160
|
+
keys: string | string[]
|
|
161
|
+
): void {
|
|
162
|
+
// If keys is a string, convert it to an array using the "." separator
|
|
163
|
+
if (typeof keys === "string") {
|
|
164
|
+
keys = keys.split(".");
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
let i = 0;
|
|
168
|
+
const l = keys.length;
|
|
169
|
+
let t = obj;
|
|
170
|
+
let k;
|
|
64
171
|
|
|
65
172
|
while (i < l - 1) {
|
|
66
173
|
k = keys[i++];
|
|
67
|
-
if (k === "__proto__" || k === "constructor" || k === "prototype") return; //
|
|
174
|
+
if (k === "__proto__" || k === "constructor" || k === "prototype") return; // Avoid dangerous keys
|
|
175
|
+
if (typeof t[k] !== "object" || t[k] === null) return; // If the object doesn't exist, stop
|
|
68
176
|
t = t[k];
|
|
69
|
-
if (typeof t !== "object" || t === null) return; // Si l'objet n'existe pas, on arrête
|
|
70
177
|
}
|
|
71
178
|
|
|
72
179
|
k = keys[i];
|
|
@@ -79,16 +186,35 @@ export function dremove(obj, keys) {
|
|
|
79
186
|
}
|
|
80
187
|
}
|
|
81
188
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
189
|
+
/**
|
|
190
|
+
* Builds an object from a map of values and updates the provided memory object.
|
|
191
|
+
*
|
|
192
|
+
* For each key-value pair in the map, this function sets the value at the given path in the `memoryObj`.
|
|
193
|
+
* If the value is "$delete", it removes the corresponding path from `allMemory`.
|
|
194
|
+
*
|
|
195
|
+
* @param {Map<string, any>} valuesMap - A map where the keys are paths and the values are the values to set at those paths.
|
|
196
|
+
* @param {Record<string, any>} allMemory - The object to update based on the values in the map.
|
|
197
|
+
* @returns {Record<string, any>} - The built memory object with the applied values from the map.
|
|
198
|
+
*
|
|
199
|
+
* @example
|
|
200
|
+
* const valuesMap = new Map();
|
|
201
|
+
* valuesMap.set('a.b.c', 1);
|
|
202
|
+
* valuesMap.set('x.y.z', '$delete');
|
|
203
|
+
* const allMemory = { x: { y: { z: 2 } } };
|
|
204
|
+
* const result = buildObject(valuesMap, allMemory);
|
|
205
|
+
* // result is { a: { b: { c: 1 } }, x: { y: { z: '$delete' } } }
|
|
206
|
+
* // allMemory is { a: { b: { c: 1 } }, x: { y: {} } }
|
|
207
|
+
*/
|
|
208
|
+
export function buildObject(valuesMap: Map<string, any>, allMemory: Record<string, any>): Record<string, any> {
|
|
209
|
+
let memoryObj = {};
|
|
210
|
+
for (let path of valuesMap.keys()) {
|
|
211
|
+
const value = valuesMap.get(path);
|
|
212
|
+
dset(memoryObj, path, value);
|
|
213
|
+
if (value === "$delete") {
|
|
214
|
+
dremove(allMemory, path);
|
|
215
|
+
} else {
|
|
216
|
+
dset(allMemory, path, value);
|
|
92
217
|
}
|
|
93
|
-
|
|
94
|
-
|
|
218
|
+
}
|
|
219
|
+
return memoryObj;
|
|
220
|
+
}
|