@colyseus/react 0.1.1 → 0.1.3

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
  /**
@@ -108,9 +108,9 @@ function createSnapshot(node, ctx) {
108
108
  const savedParentRefId = ctx.currentParentRefId;
109
109
  ctx.currentParentRefId = refId;
110
110
  let result;
111
- if (node instanceof _colyseus_schema.MapSchema) result = createSnapshotForMapSchema(node, previousResult, ctx);
112
- else if (node instanceof _colyseus_schema.ArraySchema) result = createSnapshotForArraySchema(node, previousResult, ctx);
113
- else if (node instanceof _colyseus_schema.Schema) result = createSnapshotForSchema(node, previousResult, ctx);
111
+ if (typeof node["set"] === "function") result = createSnapshotForMapSchema(node, previousResult, ctx);
112
+ else if (typeof node["push"] === "function") result = createSnapshotForArraySchema(node, previousResult, ctx);
113
+ else if (_colyseus_schema.Schema.isSchema(node)) result = createSnapshotForSchema(node, previousResult, ctx);
114
114
  else result = node;
115
115
  ctx.currentParentRefId = savedParentRefId;
116
116
  if (refId !== -1) ctx.currentResultsByRefId.set(refId, result);
@@ -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;
package/dist/index.mjs CHANGED
@@ -1,8 +1,16 @@
1
1
  import { useCallback, useEffect, useRef, useSyncExternalStore } from "react";
2
- import { ArraySchema, MapSchema, Schema } from "@colyseus/schema";
2
+ import { 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
  /**
@@ -108,9 +108,9 @@ function createSnapshot(node, ctx) {
108
108
  const savedParentRefId = ctx.currentParentRefId;
109
109
  ctx.currentParentRefId = refId;
110
110
  let result;
111
- if (node instanceof MapSchema) result = createSnapshotForMapSchema(node, previousResult, ctx);
112
- else if (node instanceof ArraySchema) result = createSnapshotForArraySchema(node, previousResult, ctx);
113
- else if (node instanceof Schema) result = createSnapshotForSchema(node, previousResult, ctx);
111
+ if (typeof node["set"] === "function") result = createSnapshotForMapSchema(node, previousResult, ctx);
112
+ else if (typeof node["push"] === "function") result = createSnapshotForArraySchema(node, previousResult, ctx);
113
+ else if (Schema.isSchema(node)) result = createSnapshotForSchema(node, previousResult, ctx);
114
114
  else result = node;
115
115
  ctx.currentParentRefId = savedParentRefId;
116
116
  if (refId !== -1) ctx.currentResultsByRefId.set(refId, result);
@@ -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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@colyseus/react",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
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",