@noya-app/noya-multiplayer-react 0.1.70 → 0.1.72

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.
@@ -2,14 +2,9 @@ import {
2
2
  ModifiedResource,
3
3
  Resource,
4
4
  ResourceCreate,
5
- ResourceCreateMap,
6
- ResourceMap,
7
5
  } from "@noya-app/noya-schemas";
8
6
  import { describe, expect, it } from "bun:test";
9
- import {
10
- buildCreateResourcePayload,
11
- resourceToCreateParameters,
12
- } from "../NoyaStateContext";
7
+ import { buildResourcePatchPayload } from "../NoyaStateContext";
13
8
 
14
9
  const ISO_DATE = "2024-01-01T00:00:00.000Z";
15
10
 
@@ -82,25 +77,23 @@ describe("resource payload helpers", () => {
82
77
  it("returns create parameters for added resources", () => {
83
78
  const addedResource = createDirectoryResourceCreate();
84
79
 
85
- const result = buildCreateResourcePayload({
80
+ const result = buildResourcePatchPayload({
86
81
  addedResources: [addedResource],
87
82
  modifiedResources: [],
88
- newResourceMap: {
89
- [addedResource.path]: addedResource,
90
- } satisfies ResourceCreateMap,
91
- resourceMap: {},
83
+ removedResources: [],
92
84
  });
93
85
 
94
- expect(result.payload).toEqual([
86
+ expect(result.create).toEqual([
95
87
  {
96
88
  path: "docs",
97
89
  type: "directory",
98
90
  },
99
91
  ]);
100
- expect(result.consumedModifiedResourceIds).toEqual([]);
92
+ expect(result.update).toBeUndefined();
93
+ expect(result.delete).toBeUndefined();
101
94
  });
102
95
 
103
- it("includes asset replacements when a modified resource swaps the asset", () => {
96
+ it("includes update entries when a modified resource swaps the asset", () => {
104
97
  const existingResource = createAssetResource({
105
98
  id: "existing-resource",
106
99
  stableId: "existing-stable",
@@ -115,117 +108,31 @@ describe("resource payload helpers", () => {
115
108
  assetId: "asset-new",
116
109
  });
117
110
 
118
- const result = buildCreateResourcePayload({
111
+ const result = buildResourcePatchPayload({
119
112
  addedResources: [],
120
113
  modifiedResources: [
121
114
  {
122
115
  id: existingResource.id,
123
116
  stableId: existingResource.stableId,
124
117
  assetId: updatedResource.assetId,
125
- } satisfies ModifiedResource,
126
- ],
127
- newResourceMap: {
128
- [updatedResource.path]: updatedResource,
129
- } satisfies ResourceCreateMap,
130
- resourceMap: {
131
- [existingResource.path]: existingResource,
132
- } satisfies ResourceMap,
133
- });
134
-
135
- expect(result.payload).toEqual([
136
- resourceToCreateParameters(updatedResource),
137
- ]);
138
- expect(result.consumedModifiedResourceIds).toEqual([
139
- existingResource.id,
140
- ]);
141
- });
142
-
143
- it("includes asset replacements when a modified resource provides inline asset data", () => {
144
- const existingResource = createAssetResource({
145
- id: "existing-resource",
146
- stableId: "existing-stable",
147
- path: "images/logo.png",
148
- assetId: "asset-old",
149
- });
150
-
151
- const updatedResource = createAssetResourceCreate({
152
- id: "existing-resource",
153
- stableId: "existing-stable",
154
- path: "images/logo.png",
155
- assetId: "asset-old",
156
- asset: {
157
- content: "ZGF0YQ==",
158
- contentType: "image/png",
159
- encoding: "base64" as const,
160
- },
161
- });
162
-
163
- const result = buildCreateResourcePayload({
164
- addedResources: [],
165
- modifiedResources: [
166
- {
167
- id: existingResource.id,
168
- stableId: existingResource.stableId,
169
118
  asset: updatedResource.asset,
170
119
  } satisfies ModifiedResource,
171
120
  ],
172
- newResourceMap: {
173
- [updatedResource.path]: updatedResource,
174
- } satisfies ResourceCreateMap,
175
- resourceMap: {
176
- [existingResource.path]: existingResource,
177
- } satisfies ResourceMap,
121
+ removedResources: [],
178
122
  });
179
123
 
180
- expect(result.payload).toEqual([
181
- resourceToCreateParameters(updatedResource),
182
- ]);
183
- expect(result.consumedModifiedResourceIds).toEqual([
184
- existingResource.id,
185
- ]);
186
- });
187
-
188
- it("deduplicates payload entries by path", () => {
189
- const existingResource = createAssetResource({
190
- id: "existing-resource",
191
- stableId: "existing-stable",
192
- path: "shared/path",
193
- assetId: "asset-old",
194
- });
195
-
196
- const updatedResource = createAssetResourceCreate({
197
- id: "new-resource",
198
- stableId: "new-stable",
199
- path: "shared/path",
200
- assetId: "asset-new",
201
- });
202
-
203
- const result = buildCreateResourcePayload({
204
- addedResources: [updatedResource],
205
- modifiedResources: [
206
- {
207
- id: existingResource.id,
208
- stableId: existingResource.stableId,
209
- assetId: updatedResource.assetId,
210
- } satisfies ModifiedResource,
211
- ],
212
- newResourceMap: {
213
- [updatedResource.path]: updatedResource,
214
- } satisfies ResourceCreateMap,
215
- resourceMap: {
216
- [existingResource.path]: existingResource,
217
- } satisfies ResourceMap,
218
- });
219
-
220
- expect(result.payload).toEqual([
221
- resourceToCreateParameters(updatedResource),
222
- ]);
223
- expect(result.consumedModifiedResourceIds).toEqual([
224
- existingResource.id,
124
+ expect(result.update).toEqual([
125
+ {
126
+ id: existingResource.id,
127
+ assetId: updatedResource.assetId,
128
+ asset: updatedResource.asset,
129
+ },
225
130
  ]);
131
+ expect(result.create).toBeUndefined();
132
+ expect(result.delete).toBeUndefined();
226
133
  });
227
134
 
228
- it("excludes modified resources without asset changes from the consumed list", () => {
135
+ it("includes delete entries for removed resources", () => {
229
136
  const existingResource = createAssetResource({
230
137
  id: "existing-resource",
231
138
  stableId: "existing-stable",
@@ -233,31 +140,14 @@ describe("resource payload helpers", () => {
233
140
  assetId: "asset-old",
234
141
  });
235
142
 
236
- const updatedResource = createAssetResourceCreate({
237
- id: "existing-resource",
238
- stableId: "existing-stable",
239
- path: "images/logo.png",
240
- assetId: "asset-old",
241
- });
242
-
243
- const result = buildCreateResourcePayload({
143
+ const result = buildResourcePatchPayload({
244
144
  addedResources: [],
245
- modifiedResources: [
246
- {
247
- id: existingResource.id,
248
- stableId: existingResource.stableId,
249
- path: updatedResource.path,
250
- } satisfies ModifiedResource,
251
- ],
252
- newResourceMap: {
253
- [updatedResource.path]: updatedResource,
254
- } satisfies ResourceCreateMap,
255
- resourceMap: {
256
- [existingResource.path]: existingResource,
257
- } satisfies ResourceMap,
145
+ modifiedResources: [],
146
+ removedResources: [existingResource],
258
147
  });
259
148
 
260
- expect(result.payload).toEqual([]);
261
- expect(result.consumedModifiedResourceIds).toEqual([]);
149
+ expect(result.delete).toEqual([{ id: existingResource.id }]);
150
+ expect(result.create).toBeUndefined();
151
+ expect(result.update).toBeUndefined();
262
152
  });
263
153
  });
@@ -3,9 +3,9 @@
3
3
  import { Observable } from "@noya-app/observable";
4
4
  import { memoGeneric } from "@noya-app/react-utils";
5
5
  import {
6
- ConnectedUsersManager,
7
- EphemeralUserDataManager,
8
6
  MultiplayerUser,
7
+ SharedConnectionDataManager,
8
+ UserManager,
9
9
  } from "@noya-app/state-manager";
10
10
  import React, { useEffect, useMemo, useState } from "react";
11
11
  import { useObservable } from "../useObservable";
@@ -26,7 +26,7 @@ export type RenderUserPointer = {
26
26
 
27
27
  type InternalUserPointerProps<E extends UserPointerData> = {
28
28
  user: MultiplayerUser;
29
- ephemeralUserDataManager: EphemeralUserDataManager<E>;
29
+ sharedConnectionDataManager: SharedConnectionDataManager<E>;
30
30
  hideAfter?: number;
31
31
  } & RenderUserPointer;
32
32
 
@@ -38,22 +38,22 @@ const UserPointerInternal = memoGeneric(function UserPointerInternal<
38
38
  E extends UserPointerData,
39
39
  >({
40
40
  user,
41
- ephemeralUserDataManager,
41
+ sharedConnectionDataManager,
42
42
  hideAfter = 5000,
43
43
  renderUserPointer,
44
44
  }: InternalUserPointerProps<E>) {
45
45
  const observable = useMemo(() => {
46
- const metadata$ = ephemeralUserDataManager.metadata$
47
- .observePath([user.id] as const)
46
+ const metadata$ = sharedConnectionDataManager.metadata$
47
+ .observePath([user.connectionId] as const)
48
48
  .throttle(50);
49
- const data$ = ephemeralUserDataManager.data$
50
- .observePath([user.id] as const)
49
+ const data$ = sharedConnectionDataManager.data$
50
+ .observePath([user.connectionId] as const)
51
51
  .throttle(50);
52
52
 
53
53
  return Observable.combine([metadata$, data$], ([metadata, data]) => {
54
54
  return { metadata, data };
55
55
  });
56
- }, [ephemeralUserDataManager, user.id]);
56
+ }, [sharedConnectionDataManager, user.connectionId]);
57
57
 
58
58
  const { metadata, data } = useObservable(observable);
59
59
 
@@ -75,7 +75,7 @@ const UserPointerInternal = memoGeneric(function UserPointerInternal<
75
75
  if (!data?.pointer) return null;
76
76
 
77
77
  return renderUserPointer({
78
- userId: user.id,
78
+ userId: user.connectionId,
79
79
  name: user.name,
80
80
  point: data.pointer,
81
81
  visible: show,
@@ -83,31 +83,33 @@ const UserPointerInternal = memoGeneric(function UserPointerInternal<
83
83
  });
84
84
 
85
85
  export type UserPointersOverlayProps<E extends UserPointerData> = {
86
- connectedUsersManager: ConnectedUsersManager;
87
- ephemeralUserDataManager: EphemeralUserDataManager<E>;
86
+ userManager: UserManager;
87
+ sharedConnectionDataManager: SharedConnectionDataManager<E>;
88
88
  } & RenderUserPointer;
89
89
 
90
90
  export const UserPointersOverlay = memoGeneric(function UserPointers<
91
91
  E extends UserPointerData,
92
92
  >({
93
- connectedUsersManager,
94
- ephemeralUserDataManager,
93
+ userManager,
94
+ sharedConnectionDataManager,
95
95
  renderUserPointer,
96
96
  }: UserPointersOverlayProps<E>) {
97
- const currentUserId = useObservable(ephemeralUserDataManager.currentUserId$);
98
- const connectedUsers = useObservable(connectedUsersManager.connectedUsers$);
97
+ const currentConnectionId = useObservable(
98
+ sharedConnectionDataManager.currentConnectionId$
99
+ );
100
+ const connectedUsers = useObservable(userManager.connectedUsers$);
99
101
 
100
102
  return (
101
103
  <>
102
104
  {connectedUsers.map((user) => {
103
- if (user.id === currentUserId) return null;
105
+ if (user.connectionId === currentConnectionId) return null;
104
106
 
105
107
  return (
106
108
  <UserPointerInternal
107
- key={user.id}
109
+ key={user.connectionId}
108
110
  user={user}
109
- ephemeralUserDataManager={
110
- ephemeralUserDataManager as unknown as EphemeralUserDataManager<UserPointerData>
111
+ sharedConnectionDataManager={
112
+ sharedConnectionDataManager as unknown as SharedConnectionDataManager<UserPointerData>
111
113
  }
112
114
  renderUserPointer={renderUserPointer}
113
115
  />
package/src/hooks.ts CHANGED
@@ -108,8 +108,9 @@ export type UseMultiplayerStateOptions<
108
108
  M extends object = object,
109
109
  E extends object = object,
110
110
  MenuT extends string = string,
111
+ I extends Record<string, any> = Record<string, unknown>,
111
112
  > = NoyaManagerOptions<S, M> & {
112
- sync?: SyncAdapter<S, M, E, MenuT>;
113
+ sync?: SyncAdapter<S, M, E, MenuT, I>;
113
114
  inspector?: boolean | StateInspectorOptions;
114
115
  };
115
116
 
@@ -119,10 +120,11 @@ export function useBindNoyaManager<
119
120
  M extends object = object,
120
121
  E extends object = object,
121
122
  MenuT extends string = string,
123
+ I extends Record<string, any> = Record<string, unknown>,
122
124
  >(
123
- noyaManager: NoyaManager<S, M, E, MenuT>,
125
+ noyaManager: NoyaManager<S, M, E, MenuT, I>,
124
126
  options?: Pick<
125
- UseMultiplayerStateOptions<S, M, E, MenuT>,
127
+ UseMultiplayerStateOptions<S, M, E, MenuT, I>,
126
128
  "inspector" | "sync"
127
129
  >
128
130
  ) {
@@ -135,8 +137,8 @@ export function useBindNoyaManager<
135
137
  noyaManager: typeof noyaManager;
136
138
  taskManager: typeof noyaManager.taskManager;
137
139
  connectionEventManager: typeof noyaManager.connectionEventManager;
138
- connectedUsersManager: typeof noyaManager.connectedUsersManager;
139
- ephemeralUserDataManager: typeof noyaManager.ephemeralUserDataManager;
140
+ userManager: typeof noyaManager.userManager;
141
+ sharedConnectionDataManager: typeof noyaManager.sharedConnectionDataManager;
140
142
  multiplayerStateManager: typeof noyaManager.multiplayerStateManager;
141
143
  assetManager: typeof noyaManager.assetManager;
142
144
  aiManager: typeof noyaManager.aiManager;
@@ -154,8 +156,8 @@ export function useBindNoyaManager<
154
156
  noyaManager,
155
157
  taskManager: noyaManager.taskManager,
156
158
  connectionEventManager: noyaManager.connectionEventManager,
157
- connectedUsersManager: noyaManager.connectedUsersManager,
158
- ephemeralUserDataManager: noyaManager.ephemeralUserDataManager,
159
+ userManager: noyaManager.userManager,
160
+ sharedConnectionDataManager: noyaManager.sharedConnectionDataManager,
159
161
  multiplayerStateManager: noyaManager.multiplayerStateManager,
160
162
  assetManager: noyaManager.assetManager,
161
163
  aiManager: noyaManager.aiManager,
@@ -179,9 +181,10 @@ export function useBindNoyaManager<
179
181
 
180
182
  useErrorOverlay<MenuT>(noyaManager);
181
183
 
182
- useStateInspector<S, M, E, MenuT>({
184
+ useStateInspector<S, M, E, MenuT, I>({
183
185
  noyaManager,
184
186
  disabled: !options?.inspector,
187
+ advanced: true,
185
188
  ...(typeof options?.inspector === "object" && options?.inspector),
186
189
  });
187
190
 
@@ -193,30 +196,35 @@ export function useMultiplayerState<
193
196
  M extends object = object,
194
197
  E extends object = object,
195
198
  MenuT extends string = string,
199
+ I extends Record<string, any> = Record<string, unknown>,
196
200
  >(
197
201
  initialState: S | (() => S),
198
- options?: UseMultiplayerStateOptions<S, M, E, MenuT>
202
+ options?: UseMultiplayerStateOptions<S, M, E, MenuT, I>
199
203
  ) {
200
204
  const {
201
205
  sync = stubSync<
202
206
  unknown,
203
207
  object,
204
208
  object,
205
- string
206
- >() as unknown as SyncAdapter<S, M, E, MenuT>,
209
+ string,
210
+ Record<string, unknown>
211
+ >() as unknown as SyncAdapter<S, M, E, MenuT, I>,
207
212
  inspector,
208
213
  ...rest
209
214
  } = options ?? {};
210
215
 
211
216
  const [noyaManager] = useState(
212
- () => new NoyaManager<S, M, E, MenuT>(initialState, rest)
217
+ () => new NoyaManager<S, M, E, MenuT, I>(initialState, rest)
213
218
  );
214
219
 
215
- return useBindNoyaManager<S, M, E, MenuT>(noyaManager, { sync, inspector });
220
+ return useBindNoyaManager<S, M, E, MenuT, I>(noyaManager, {
221
+ sync,
222
+ inspector,
223
+ });
216
224
  }
217
225
 
218
226
  function useErrorOverlay<MenuT extends string = string>(
219
- noyaManager: NoyaManager<any, any, any, MenuT>
227
+ noyaManager: NoyaManager<any, any, any, MenuT, any>
220
228
  ) {
221
229
  const unrecoverableError = useObservable(noyaManager.unrecoverableError);
222
230