@colyseus/react 0.1.0 → 0.1.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.cjs CHANGED
@@ -3,6 +3,14 @@ let _colyseus_schema = require("@colyseus/schema");
3
3
 
4
4
  //#region src/schema/createSnapshot.ts
5
5
  /**
6
+ * Returns the `@type`-decorated field names for a Schema class,
7
+ * reading from `Symbol.metadata` set by v4's `@type()` decorators.
8
+ */
9
+ function getSchemaFieldNames(node) {
10
+ const metadata = node.constructor?.[Symbol.metadata];
11
+ if (metadata && typeof metadata === "object") return Object.values(metadata).map((f) => f.name);
12
+ }
13
+ /**
6
14
  * Builds a reverse lookup map from objects to their refIds.
7
15
  */
8
16
  function buildObjectToRefIdMap(refs) {
@@ -64,8 +72,9 @@ function createSnapshotForArraySchema(node, previousResult, ctx) {
64
72
  function createSnapshotForSchema(node, previousResult, ctx) {
65
73
  const snapshotted = {};
66
74
  let hasChanged = previousResult === void 0;
67
- const fieldDefinitions = node._definition?.fields;
68
- if (fieldDefinitions) for (const fieldName in fieldDefinitions) {
75
+ const fieldNames = getSchemaFieldNames(node);
76
+ if (!fieldNames) throw new Error(`createSnapshotForSchema: no field metadata found on ${node.constructor?.name ?? "unknown"}. Is @colyseus/schema v4 installed?`);
77
+ for (const fieldName of fieldNames) {
69
78
  const value = node[fieldName];
70
79
  if (typeof value !== "function") {
71
80
  const snapshottedValue = createSnapshot(value, ctx);
@@ -73,15 +82,6 @@ function createSnapshotForSchema(node, previousResult, ctx) {
73
82
  if (!hasChanged && previousResult && previousResult[fieldName] !== snapshottedValue) hasChanged = true;
74
83
  }
75
84
  }
76
- else for (const key in node) {
77
- if (key.startsWith("_") || key.startsWith("$")) continue;
78
- const value = node[key];
79
- if (typeof value !== "function") {
80
- const snapshottedValue = createSnapshot(value, ctx);
81
- snapshotted[key] = snapshottedValue;
82
- if (!hasChanged && previousResult && previousResult[key] !== snapshottedValue) hasChanged = true;
83
- }
84
- }
85
85
  return hasChanged ? snapshotted : previousResult;
86
86
  }
87
87
  /**
@@ -125,7 +125,14 @@ const subscriptionsByState = /* @__PURE__ */ new WeakMap();
125
125
  * Gets or creates a subscription for the given room state.
126
126
  *
127
127
  * This function sets up change notification by wrapping the decoder's
128
- * `triggerChanges` method to notify all subscribed React components.
128
+ * `decode` method to intercept all state changes and notify subscribed
129
+ * React components.
130
+ *
131
+ * We wrap `decode()` rather than assigning `decoder.triggerChanges` because
132
+ * `triggerChanges` is a single-slot callback that gets unconditionally
133
+ * overwritten by `Callbacks.get()` / `getDecoderStateCallbacks()` on every
134
+ * call. Wrapping `decode()` sidesteps this conflict entirely since it is the
135
+ * sole entry point for both full-state syncs and incremental patches.
129
136
  *
130
137
  * @param roomState - The Colyseus room state Schema instance
131
138
  * @param decoder - The Colyseus decoder associated with the room
@@ -140,27 +147,30 @@ function getOrCreateSubscription(roomState, decoder) {
140
147
  dirtyRefIds: /* @__PURE__ */ new Set(),
141
148
  parentRefIdMap: /* @__PURE__ */ new Map(),
142
149
  objectToRefId: void 0,
143
- cleanupCounter: 0
150
+ cleanupCounter: 0,
151
+ originalDecode: decoder.decode
144
152
  };
145
- subscription.originalTrigger = decoder.triggerChanges?.bind(decoder);
146
- decoder.triggerChanges = (changes) => {
147
- if (subscription.originalTrigger) subscription.originalTrigger(changes);
148
- const refs = decoder.root?.refs;
149
- if (refs) {
150
- subscription.objectToRefId = /* @__PURE__ */ new Map();
151
- for (const [refId, obj] of refs.entries()) if (obj !== null && typeof obj === "object") subscription.objectToRefId.set(obj, refId);
152
- }
153
- for (const change of changes) {
154
- const refId = subscription.objectToRefId?.get(change.ref) ?? -1;
155
- if (refId !== -1) {
156
- let currentRefId = refId;
157
- while (currentRefId !== void 0) {
158
- subscription.dirtyRefIds.add(currentRefId);
159
- currentRefId = subscription.parentRefIdMap.get(currentRefId);
153
+ decoder.decode = function(...args) {
154
+ const changes = subscription.originalDecode.apply(decoder, args);
155
+ if (changes && changes.length > 0) {
156
+ const refs = decoder.root?.refs;
157
+ if (refs) {
158
+ subscription.objectToRefId = /* @__PURE__ */ new Map();
159
+ for (const [refId, obj] of refs.entries()) if (obj !== null && typeof obj === "object") subscription.objectToRefId.set(obj, refId);
160
+ }
161
+ for (const change of changes) {
162
+ const refId = subscription.objectToRefId?.get(change.ref) ?? -1;
163
+ if (refId !== -1) {
164
+ let currentRefId = refId;
165
+ while (currentRefId !== void 0) {
166
+ subscription.dirtyRefIds.add(currentRefId);
167
+ currentRefId = subscription.parentRefIdMap.get(currentRefId);
168
+ }
160
169
  }
161
170
  }
171
+ subscription.listeners.forEach((callback) => callback());
162
172
  }
163
- subscription.listeners.forEach((callback) => callback());
173
+ return changes;
164
174
  };
165
175
  subscriptionsByState.set(roomState, subscription);
166
176
  return subscription;
@@ -199,10 +209,12 @@ function useColyseusState(roomState, decoder, selector = (s) => s) {
199
209
  (0, react.useEffect)(() => {
200
210
  if (roomState && decoder) getOrCreateSubscription(roomState, decoder);
201
211
  }, [roomState, decoder]);
202
- const getSnapshot = () => {
212
+ const selectorRef = (0, react.useRef)(selector);
213
+ selectorRef.current = selector;
214
+ const getSnapshot = (0, react.useCallback)(() => {
203
215
  if (!roomState || !decoder) return;
204
216
  const subscription = getOrCreateSubscription(roomState, decoder);
205
- const selectedState = selector(roomState);
217
+ const selectedState = selectorRef.current(roomState);
206
218
  const ctx = {
207
219
  refs: decoder.root?.refs,
208
220
  objectToRefId: subscription.objectToRefId,
@@ -224,14 +236,13 @@ function useColyseusState(roomState, decoder, selector = (s) => s) {
224
236
  }
225
237
  }
226
238
  return result;
227
- };
228
- const subscribe = (callback) => {
239
+ }, [roomState, decoder]);
240
+ return (0, react.useSyncExternalStore)((0, react.useCallback)((callback) => {
229
241
  if (!roomState || !decoder) return () => {};
230
242
  const subscription = getOrCreateSubscription(roomState, decoder);
231
243
  subscription.listeners.add(callback);
232
244
  return () => subscription.listeners.delete(callback);
233
- };
234
- return (0, react.useSyncExternalStore)(subscribe, getSnapshot);
245
+ }, [roomState, decoder]), getSnapshot);
235
246
  }
236
247
 
237
248
  //#endregion
package/dist/index.mjs CHANGED
@@ -1,8 +1,16 @@
1
- import { useEffect, useSyncExternalStore } from "react";
1
+ import { useCallback, useEffect, useRef, useSyncExternalStore } from "react";
2
2
  import { ArraySchema, MapSchema, Schema } from "@colyseus/schema";
3
3
 
4
4
  //#region src/schema/createSnapshot.ts
5
5
  /**
6
+ * Returns the `@type`-decorated field names for a Schema class,
7
+ * reading from `Symbol.metadata` set by v4's `@type()` decorators.
8
+ */
9
+ function getSchemaFieldNames(node) {
10
+ const metadata = node.constructor?.[Symbol.metadata];
11
+ if (metadata && typeof metadata === "object") return Object.values(metadata).map((f) => f.name);
12
+ }
13
+ /**
6
14
  * Builds a reverse lookup map from objects to their refIds.
7
15
  */
8
16
  function buildObjectToRefIdMap(refs) {
@@ -64,8 +72,9 @@ function createSnapshotForArraySchema(node, previousResult, ctx) {
64
72
  function createSnapshotForSchema(node, previousResult, ctx) {
65
73
  const snapshotted = {};
66
74
  let hasChanged = previousResult === void 0;
67
- const fieldDefinitions = node._definition?.fields;
68
- if (fieldDefinitions) for (const fieldName in fieldDefinitions) {
75
+ const fieldNames = getSchemaFieldNames(node);
76
+ if (!fieldNames) throw new Error(`createSnapshotForSchema: no field metadata found on ${node.constructor?.name ?? "unknown"}. Is @colyseus/schema v4 installed?`);
77
+ for (const fieldName of fieldNames) {
69
78
  const value = node[fieldName];
70
79
  if (typeof value !== "function") {
71
80
  const snapshottedValue = createSnapshot(value, ctx);
@@ -73,15 +82,6 @@ function createSnapshotForSchema(node, previousResult, ctx) {
73
82
  if (!hasChanged && previousResult && previousResult[fieldName] !== snapshottedValue) hasChanged = true;
74
83
  }
75
84
  }
76
- else for (const key in node) {
77
- if (key.startsWith("_") || key.startsWith("$")) continue;
78
- const value = node[key];
79
- if (typeof value !== "function") {
80
- const snapshottedValue = createSnapshot(value, ctx);
81
- snapshotted[key] = snapshottedValue;
82
- if (!hasChanged && previousResult && previousResult[key] !== snapshottedValue) hasChanged = true;
83
- }
84
- }
85
85
  return hasChanged ? snapshotted : previousResult;
86
86
  }
87
87
  /**
@@ -125,7 +125,14 @@ const subscriptionsByState = /* @__PURE__ */ new WeakMap();
125
125
  * Gets or creates a subscription for the given room state.
126
126
  *
127
127
  * This function sets up change notification by wrapping the decoder's
128
- * `triggerChanges` method to notify all subscribed React components.
128
+ * `decode` method to intercept all state changes and notify subscribed
129
+ * React components.
130
+ *
131
+ * We wrap `decode()` rather than assigning `decoder.triggerChanges` because
132
+ * `triggerChanges` is a single-slot callback that gets unconditionally
133
+ * overwritten by `Callbacks.get()` / `getDecoderStateCallbacks()` on every
134
+ * call. Wrapping `decode()` sidesteps this conflict entirely since it is the
135
+ * sole entry point for both full-state syncs and incremental patches.
129
136
  *
130
137
  * @param roomState - The Colyseus room state Schema instance
131
138
  * @param decoder - The Colyseus decoder associated with the room
@@ -140,27 +147,30 @@ function getOrCreateSubscription(roomState, decoder) {
140
147
  dirtyRefIds: /* @__PURE__ */ new Set(),
141
148
  parentRefIdMap: /* @__PURE__ */ new Map(),
142
149
  objectToRefId: void 0,
143
- cleanupCounter: 0
150
+ cleanupCounter: 0,
151
+ originalDecode: decoder.decode
144
152
  };
145
- subscription.originalTrigger = decoder.triggerChanges?.bind(decoder);
146
- decoder.triggerChanges = (changes) => {
147
- if (subscription.originalTrigger) subscription.originalTrigger(changes);
148
- const refs = decoder.root?.refs;
149
- if (refs) {
150
- subscription.objectToRefId = /* @__PURE__ */ new Map();
151
- for (const [refId, obj] of refs.entries()) if (obj !== null && typeof obj === "object") subscription.objectToRefId.set(obj, refId);
152
- }
153
- for (const change of changes) {
154
- const refId = subscription.objectToRefId?.get(change.ref) ?? -1;
155
- if (refId !== -1) {
156
- let currentRefId = refId;
157
- while (currentRefId !== void 0) {
158
- subscription.dirtyRefIds.add(currentRefId);
159
- currentRefId = subscription.parentRefIdMap.get(currentRefId);
153
+ decoder.decode = function(...args) {
154
+ const changes = subscription.originalDecode.apply(decoder, args);
155
+ if (changes && changes.length > 0) {
156
+ const refs = decoder.root?.refs;
157
+ if (refs) {
158
+ subscription.objectToRefId = /* @__PURE__ */ new Map();
159
+ for (const [refId, obj] of refs.entries()) if (obj !== null && typeof obj === "object") subscription.objectToRefId.set(obj, refId);
160
+ }
161
+ for (const change of changes) {
162
+ const refId = subscription.objectToRefId?.get(change.ref) ?? -1;
163
+ if (refId !== -1) {
164
+ let currentRefId = refId;
165
+ while (currentRefId !== void 0) {
166
+ subscription.dirtyRefIds.add(currentRefId);
167
+ currentRefId = subscription.parentRefIdMap.get(currentRefId);
168
+ }
160
169
  }
161
170
  }
171
+ subscription.listeners.forEach((callback) => callback());
162
172
  }
163
- subscription.listeners.forEach((callback) => callback());
173
+ return changes;
164
174
  };
165
175
  subscriptionsByState.set(roomState, subscription);
166
176
  return subscription;
@@ -199,10 +209,12 @@ function useColyseusState(roomState, decoder, selector = (s) => s) {
199
209
  useEffect(() => {
200
210
  if (roomState && decoder) getOrCreateSubscription(roomState, decoder);
201
211
  }, [roomState, decoder]);
202
- const getSnapshot = () => {
212
+ const selectorRef = useRef(selector);
213
+ selectorRef.current = selector;
214
+ const getSnapshot = useCallback(() => {
203
215
  if (!roomState || !decoder) return;
204
216
  const subscription = getOrCreateSubscription(roomState, decoder);
205
- const selectedState = selector(roomState);
217
+ const selectedState = selectorRef.current(roomState);
206
218
  const ctx = {
207
219
  refs: decoder.root?.refs,
208
220
  objectToRefId: subscription.objectToRefId,
@@ -224,14 +236,13 @@ function useColyseusState(roomState, decoder, selector = (s) => s) {
224
236
  }
225
237
  }
226
238
  return result;
227
- };
228
- const subscribe = (callback) => {
239
+ }, [roomState, decoder]);
240
+ return useSyncExternalStore(useCallback((callback) => {
229
241
  if (!roomState || !decoder) return () => {};
230
242
  const subscription = getOrCreateSubscription(roomState, decoder);
231
243
  subscription.listeners.add(callback);
232
244
  return () => subscription.listeners.delete(callback);
233
- };
234
- return useSyncExternalStore(subscribe, getSnapshot);
245
+ }, [roomState, decoder]), getSnapshot);
235
246
  }
236
247
 
237
248
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@colyseus/react",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "dev": "tsdown --watch --format esm,cjs",
@@ -22,8 +22,7 @@
22
22
  "peerDependencies": {
23
23
  "@colyseus/schema": "^4.0.8",
24
24
  "@colyseus/sdk": "^0.17.26",
25
- "react": "^18.3.1",
26
- "react-dom": "^18.3.1"
25
+ "react": ">=18.3.1"
27
26
  },
28
27
  "devDependencies": {
29
28
  "@colyseus/schema": "^4.0.8",
@@ -31,15 +30,12 @@
31
30
  "@eslint/js": "^9.9.0",
32
31
  "@testing-library/react": "^16.3.1",
33
32
  "@types/react": "^18.3.3",
34
- "@types/react-dom": "^18.3.0",
35
33
  "buffer": "^6.0.3",
36
34
  "eslint": "^9.9.0",
37
35
  "eslint-plugin-react-hooks": "^5.1.0-rc.0",
38
36
  "eslint-plugin-react-refresh": "^0.4.9",
39
37
  "globals": "^15.9.0",
40
38
  "happy-dom": "^20.0.11",
41
- "react": "^18.3.1",
42
- "react-dom": "^18.3.1",
43
39
  "reflect-metadata": "^0.2.2",
44
40
  "tsdown": "^0.20.1",
45
41
  "typescript": "^5.5.3",