@sanity/workflow-studio 0.9.0 → 0.20.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,49 @@
1
1
  # @sanity/workflow-studio
2
2
 
3
+ ## 0.20.0
4
+
5
+ ### Minor Changes
6
+
7
+ - e860898: **BREAKING:** Published Editorial Workflows packages now ship as one fixed release stack and require exact-version peers for every shared runtime package. Install the matching stack so the engine, reactive core, adapters, tools, and UI cannot silently load private or version-skewed copies.
8
+ - e3122cc: `useWorkflowSession` gains `guardScope` — the co-mounted session id-set passed through to the observer so guard observation consolidates into one shared live query per resource.
9
+
10
+ ### Patch Changes
11
+
12
+ - bce09fc: Restore automatic effect request tags while preserving concrete handler client APIs, backed by native request-prefix support in the test client.
13
+ - ab5e454: Keep one indexed project-member directory per Studio client, batch profile requests, and back off failed loads.
14
+ - Updated dependencies [ab5e454]
15
+ - Updated dependencies [bce09fc]
16
+ - Updated dependencies [efb4cd9]
17
+ - Updated dependencies [46b0285]
18
+ - Updated dependencies [ab5e454]
19
+ - Updated dependencies [8a76c67]
20
+ - Updated dependencies [e3122cc]
21
+ - Updated dependencies [e3122cc]
22
+ - Updated dependencies [7e8f459]
23
+ - Updated dependencies [98e488e]
24
+ - Updated dependencies [7c4dd86]
25
+ - Updated dependencies [e860898]
26
+ - @sanity/workflow-sdk@0.20.0
27
+ - @sanity/workflow-engine@0.20.0
28
+ - @sanity/workflow-react@0.20.0
29
+
30
+ ## 0.10.0
31
+
32
+ ### Minor Changes
33
+
34
+ - 767497c: **BREAKING:** reactive instance-list reads are per-row tolerant. `WorkflowInstanceList` replaces `invalid: InvalidDoc | undefined` with `unreadable: readonly InvalidDoc[]`: rows the engine's read discipline refuses are skipped from `instances` and flagged there, so one bad row no longer sinks the whole listing (a silently short list stays impossible — the refused rows are always named). `ObservedInstances` becomes `{instances, unreadable}` to carry the same split through the observer contract. The engine's own stateless list reads stay wholesale; single-instance and guard streams keep their explicit invalid states.
35
+
36
+ ### Patch Changes
37
+
38
+ - Updated dependencies [24b11dd]
39
+ - Updated dependencies [2005ab7]
40
+ - Updated dependencies [767497c]
41
+ - Updated dependencies [24b11dd]
42
+ - Updated dependencies [59b40fd]
43
+ - @sanity/workflow-engine@0.19.0
44
+ - @sanity/workflow-react@0.14.0
45
+ - @sanity/workflow-sdk@0.11.0
46
+
3
47
  ## 0.9.0
4
48
 
5
49
  ### Minor Changes
package/README.md CHANGED
@@ -26,17 +26,19 @@ Studio source context (`useSource` / `useClient`).
26
26
  set) instead of an eternal spinner.
27
27
  - `useWorkflowInstances` / `useDocumentWorkflows` — live instance lists
28
28
  (filterable collection / per-document discovery), each returning
29
- `{instances, loading, invalid, error}` through the App SDK query store. Hooks
30
- under one Studio source share the auto-bootstrapped SDK instance; callers can
31
- instead pass a shared `sdk` explicitly. SDK store errors surface reactively
32
- through `error`.
29
+ `{instances, loading, unreadable, error}` through the App SDK query store
30
+ rows the read discipline refuses are skipped from `instances` and flagged
31
+ in `unreadable`. Hooks under one Studio source share the auto-bootstrapped
32
+ SDK instance; callers can instead pass a shared `sdk` explicitly. SDK store
33
+ errors surface reactively through `error`.
33
34
  - `useStudioProjectUsers` — the shared Studio project-user directory used by
34
35
  actor resolution and member pickers. It returns the upstream membership and
35
36
  profile records without an Editorial Workflows projection, plus the engine's
36
- `ProjectUserDirectory` integration. Concurrent loads are coalesced and a
37
- settled snapshot is reused for 30 seconds before mutable user data is
38
- refreshed. `studioProjectUserDirectory` provides the same contract outside
39
- React when a Studio client and project ID are already available.
37
+ `ProjectUserDirectory` integration. Concurrent loads are coalesced, the
38
+ settled successful directory is reused for the client/project lifetime, and
39
+ failed loads retry after a 30-second backoff. `studioProjectUserDirectory`
40
+ provides the same contract outside React when a Studio client and project ID
41
+ are already available.
40
42
 
41
43
  ## How observation routes
42
44
 
@@ -84,6 +86,8 @@ This adapter composes `@sanity/workflow-sdk`'s store layer through its headless
84
86
  `./observer` entry. What that means for installs:
85
87
 
86
88
  - **`@sanity/sdk` is required** (peer) — it addresses every observed resource.
89
+ - **`@sanity/workflow-engine` is required** (peer) — the host and adapter share one engine runtime.
90
+ - **`@sanity/workflow-react` and `@sanity/workflow-sdk` are required** (exact-version peers) — install the matching Editorial Workflows release stack so the reactive context and SDK store are single-sourced.
87
91
  - **`@sanity/sdk-react` is NOT required** — nothing here touches the App
88
92
  SDK's React binding.
89
93
 
package/dist/index.cjs CHANGED
@@ -4,7 +4,7 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: !0
5
5
  });
6
6
 
7
- var observer = require("@sanity/workflow-react/observer"), observer$1 = require("@sanity/workflow-sdk/observer"), sdk = require("@sanity/sdk"), workflowEngine = require("@sanity/workflow-engine"), react = require("react"), sanity = require("sanity"), workflowReact = require("@sanity/workflow-react");
7
+ var observer = require("@sanity/workflow-react/observer"), observer$1 = require("@sanity/workflow-sdk/observer"), sdk = require("@sanity/sdk"), workflowEngine = require("@sanity/workflow-engine"), react = require("react"), sanity = require("sanity"), workflowReact = require("@sanity/workflow-react"), projectUsers = require("@sanity/workflow-sdk/project-users");
8
8
 
9
9
  function makeStudioObserver(sdk2, options) {
10
10
  const {client: client, engineResource: engineResource} = options;
@@ -142,7 +142,7 @@ function studioResourceClients(client) {
142
142
  }) : void 0;
143
143
  }
144
144
 
145
- function useWorkflowSession({engine: engine, instanceId: instanceId, grantsFromPath: grantsFromPath, sdk: sdk2}) {
145
+ function useWorkflowSession({engine: engine, instanceId: instanceId, grantsFromPath: grantsFromPath, guardScope: guardScope, sdk: sdk2}) {
146
146
  const observer2 = useStudioObserver({
147
147
  engine: engine,
148
148
  sdk: sdk2
@@ -152,6 +152,9 @@ function useWorkflowSession({engine: engine, instanceId: instanceId, grantsFromP
152
152
  instanceId: instanceId,
153
153
  observer: observer2,
154
154
  host: "studio",
155
+ ...guardScope !== void 0 ? {
156
+ guardScope: guardScope
157
+ } : {},
155
158
  opts: {
156
159
  grantsFromPath: grantsFromPath
157
160
  }
@@ -184,7 +187,7 @@ function useDocumentWorkflows({engine: engine, document: document, sdk: sdk2}) {
184
187
  });
185
188
  }
186
189
 
187
- const PROJECT_USERS_TTL_MS = 3e4;
190
+ const PROJECT_USERS_RETRY_MS = 3e4;
188
191
 
189
192
  function isRecord(value) {
190
193
  return typeof value == "object" && value !== null && !Array.isArray(value);
@@ -198,10 +201,6 @@ function isMembership(value) {
198
201
  return isRecord(value) && typeof value.id == "string" && (value.isRobot === void 0 || typeof value.isRobot == "boolean") && (value.roles === void 0 || Array.isArray(value.roles) && value.roles.every(isRole));
199
202
  }
200
203
 
201
- function isUser(value) {
202
- return isRecord(value) && typeof value.id == "string" && (value.email === void 0 || typeof value.email == "string") && (value.displayName === void 0 || typeof value.displayName == "string") && (value.imageUrl === void 0 || value.imageUrl === null || typeof value.imageUrl == "string");
203
- }
204
-
205
204
  async function fetchProjectUsers(client, projectId) {
206
205
  const project = await client.request({
207
206
  uri: `/projects/${projectId}`
@@ -211,9 +210,11 @@ async function fetchProjectUsers(client, projectId) {
211
210
  if (!membershipRows.every(isMembership)) throw new Error("Studio project user response contained an invalid membership row");
212
211
  const memberships = membershipRows.filter(member => !member.isRobot);
213
212
  if (memberships.length === 0) return [];
214
- const ids = memberships.map(member => encodeURIComponent(member.id)), response = await client.request({
215
- uri: `/projects/${projectId}/users/${ids.join(",")}`
216
- }), profiles = (Array.isArray(response) ? response : [ response ]).filter(isUser), profileById = new Map(profiles.map(profile => [ profile.id, profile ]));
213
+ const profiles = await projectUsers.loadProjectUserProfiles({
214
+ client: client,
215
+ projectId: projectId,
216
+ ids: memberships.map(member => member.id)
217
+ }), profileById = new Map(profiles.map(profile => [ profile.id, profile ]));
217
218
  return memberships.map(membership => ({
218
219
  membership: membership,
219
220
  profile: profileById.get(membership.id)
@@ -222,6 +223,40 @@ async function fetchProjectUsers(client, projectId) {
222
223
 
223
224
  const projectUsersByClient = /* @__PURE__ */ new WeakMap;
224
225
 
226
+ function createProjectUsersStore(client, projectId) {
227
+ let snapshot = {
228
+ users: [],
229
+ loading: !1,
230
+ error: void 0
231
+ }, pending, retryAt = 0, usersById = /* @__PURE__ */ new Map, usersByEmail = /* @__PURE__ */ new Map;
232
+ const listeners = /* @__PURE__ */ new Set, setSnapshot = next => {
233
+ snapshot = next, listeners.forEach(listener => listener());
234
+ };
235
+ return {
236
+ getSnapshot: () => snapshot,
237
+ load: () => pending || (snapshot.users.length > 0 || snapshot.error === void 0 && retryAt === 1 / 0 ? Promise.resolve(snapshot.users) : snapshot.error !== void 0 && Date.now() < retryAt ? Promise.reject(snapshot.error) : (setSnapshot({
238
+ ...snapshot,
239
+ loading: !0,
240
+ error: void 0
241
+ }), pending = fetchProjectUsers(client, projectId).then(users => (usersById = new Map(users.map(user => [ user.membership.id, user ])),
242
+ usersByEmail = new Map(users.flatMap(user => user.profile?.email ? [ [ user.profile.email.toLowerCase(), user ] ] : [])),
243
+ retryAt = 1 / 0, setSnapshot({
244
+ users: users,
245
+ loading: !1,
246
+ error: void 0
247
+ }), pending = void 0, users), error => {
248
+ throw retryAt = Date.now() + PROJECT_USERS_RETRY_MS, setSnapshot({
249
+ users: [],
250
+ loading: !1,
251
+ error: error
252
+ }), pending = void 0, error;
253
+ }), pending)),
254
+ subscribe: listener => (listeners.add(listener), () => listeners.delete(listener)),
255
+ userByEmail: email => usersByEmail.get(email.toLowerCase()),
256
+ userById: id => usersById.get(id)
257
+ };
258
+ }
259
+
225
260
  function clientProjectUsers(client) {
226
261
  const cached = projectUsersByClient.get(client);
227
262
  if (cached) return cached;
@@ -229,27 +264,20 @@ function clientProjectUsers(client) {
229
264
  return projectUsersByClient.set(client, created), created;
230
265
  }
231
266
 
232
- function loadProjectUsers(client, projectId) {
233
- const projectUsers = clientProjectUsers(client), cached = projectUsers.get(projectId);
234
- if (cached && cached.expiresAt > Date.now()) return cached.promise;
235
- projectUsers.delete(projectId);
236
- const pending = fetchProjectUsers(client, projectId), entry = {
237
- promise: pending,
238
- expiresAt: Number.POSITIVE_INFINITY
239
- };
240
- return projectUsers.set(projectId, entry), pending.then(() => {
241
- entry.expiresAt = Date.now() + PROJECT_USERS_TTL_MS;
242
- }, () => {
243
- projectUsers.get(projectId) === entry && projectUsers.delete(projectId);
244
- }), pending;
267
+ function projectUsersStore(client, projectId) {
268
+ const projectUsers2 = clientProjectUsers(client), cached = projectUsers2.get(projectId);
269
+ if (cached) return cached;
270
+ const created = createProjectUsersStore(client, projectId);
271
+ return projectUsers2.set(projectId, created), created;
245
272
  }
246
273
 
247
274
  function studioProjectUserDirectory(client, projectId) {
248
- const load = () => loadProjectUsers(client, projectId);
275
+ const store = projectUsersStore(client, projectId);
249
276
  return {
250
277
  findById: async id => {
251
278
  try {
252
- const user = (await load()).find(candidate => candidate.membership.id === id);
279
+ await store.load();
280
+ const user = store.userById(id);
253
281
  return user ? {
254
282
  status: "resolved",
255
283
  user: user
@@ -265,7 +293,8 @@ function studioProjectUserDirectory(client, projectId) {
265
293
  },
266
294
  findByEmail: async email => {
267
295
  try {
268
- const normalized = email.toLowerCase(), user = (await load()).find(candidate => candidate.profile?.email?.toLowerCase() === normalized);
296
+ await store.load();
297
+ const user = store.userByEmail(email);
269
298
  return user ? {
270
299
  status: "resolved",
271
300
  user: user
@@ -282,44 +311,19 @@ function studioProjectUserDirectory(client, projectId) {
282
311
  };
283
312
  }
284
313
 
314
+ const missingProjectSnapshot = {
315
+ users: [],
316
+ loading: !1,
317
+ error: new Error("No projectId on the Studio client")
318
+ }, noopSubscribe = () => () => {};
319
+
285
320
  function useStudioProjectUsers() {
286
321
  const client = sanity.useClient({
287
322
  apiVersion: workflowEngine.ENGINE_API_VERSION
288
- }), projectId = client.config().projectId, directory = react.useMemo(() => projectId ? studioProjectUserDirectory(client, projectId) : void 0, [ client, projectId ]), [state, setState] = react.useState({
289
- users: [],
290
- loading: projectId !== void 0,
291
- error: projectId === void 0 ? new Error("No projectId on the Studio client") : void 0
292
- });
323
+ }), projectId = client.config().projectId, directory = react.useMemo(() => projectId ? studioProjectUserDirectory(client, projectId) : void 0, [ client, projectId ]), store = react.useMemo(() => projectId ? projectUsersStore(client, projectId) : void 0, [ client, projectId ]), state = react.useSyncExternalStore(store?.subscribe ?? noopSubscribe, store?.getSnapshot ?? (() => missingProjectSnapshot), store?.getSnapshot ?? (() => missingProjectSnapshot));
293
324
  return react.useEffect(() => {
294
- if (!projectId) {
295
- setState({
296
- users: [],
297
- loading: !1,
298
- error: new Error("No projectId on the Studio client")
299
- });
300
- return;
301
- }
302
- let cancelled = !1;
303
- return setState({
304
- users: [],
305
- loading: !0,
306
- error: void 0
307
- }), loadProjectUsers(client, projectId).then(users => {
308
- cancelled || setState({
309
- users: users,
310
- loading: !1,
311
- error: void 0
312
- });
313
- }, error => {
314
- cancelled || setState({
315
- users: [],
316
- loading: !1,
317
- error: error
318
- });
319
- }), () => {
320
- cancelled = !0;
321
- };
322
- }, [ client, projectId ]), {
325
+ store && store.load().catch(() => {});
326
+ }, [ store ]), {
323
327
  ...state,
324
328
  directory: directory
325
329
  };
package/dist/index.d.cts CHANGED
@@ -7,10 +7,10 @@ import type { InstancesQueryFilter } from "@sanity/workflow-engine";
7
7
  import { MissingHandlerPolicy } from "@sanity/workflow-engine";
8
8
  import { MutationGuardDoc } from "@sanity/workflow-react";
9
9
  import { ProjectUserDirectory } from "@sanity/workflow-engine";
10
+ import { ProjectUserProfile } from "@sanity/workflow-sdk/project-users";
10
11
  import { ResourceClientResolver } from "@sanity/workflow-engine";
11
12
  import { SanityClient } from "sanity";
12
13
  import { SanityInstance } from "@sanity/sdk";
13
- import { User } from "sanity";
14
14
  import { WorkflowInstanceList } from "@sanity/workflow-react";
15
15
  import { WorkflowObserver } from "@sanity/workflow-react/observer";
16
16
  import { WorkflowResource } from "@sanity/workflow-engine";
@@ -81,12 +81,13 @@ declare interface StudioUserClient {
81
81
  readonly config: () => {
82
82
  readonly projectId?: string;
83
83
  };
84
- readonly request: (options: { readonly uri: string }) => Promise<unknown>;
84
+ readonly request: (options: {
85
+ readonly uri: string;
86
+ readonly tag?: string;
87
+ }) => Promise<unknown>;
85
88
  }
86
89
 
87
- /** Native Studio profile response, whose nullable avatar is wider than Sanity's public type. */
88
- export declare interface StudioUserProfile extends Omit<User, "imageUrl"> {
89
- readonly imageUrl?: string | null;
90
+ export declare interface StudioUserProfile extends ProjectUserProfile {
90
91
  readonly [key: string]: unknown;
91
92
  }
92
93
 
@@ -173,11 +174,17 @@ export declare function useWorkflowSession({
173
174
  engine,
174
175
  instanceId,
175
176
  grantsFromPath,
177
+ guardScope,
176
178
  sdk,
177
179
  }: {
178
180
  engine: Engine;
179
181
  instanceId: string;
180
182
  grantsFromPath?: string;
183
+ /** The id-set of every co-mounted session, handed identically to each one
184
+ * so guard observation consolidates into one shared live query per
185
+ * resource. Must include `instanceId`. Omitted, guards are observed per
186
+ * instance. */
187
+ guardScope?: readonly string[];
181
188
  sdk?: SanityInstance;
182
189
  }): WorkflowSession;
183
190
 
package/dist/index.d.ts CHANGED
@@ -7,10 +7,10 @@ import type { InstancesQueryFilter } from "@sanity/workflow-engine";
7
7
  import { MissingHandlerPolicy } from "@sanity/workflow-engine";
8
8
  import { MutationGuardDoc } from "@sanity/workflow-react";
9
9
  import { ProjectUserDirectory } from "@sanity/workflow-engine";
10
+ import { ProjectUserProfile } from "@sanity/workflow-sdk/project-users";
10
11
  import { ResourceClientResolver } from "@sanity/workflow-engine";
11
12
  import { SanityClient } from "sanity";
12
13
  import { SanityInstance } from "@sanity/sdk";
13
- import { User } from "sanity";
14
14
  import { WorkflowInstanceList } from "@sanity/workflow-react";
15
15
  import { WorkflowObserver } from "@sanity/workflow-react/observer";
16
16
  import { WorkflowResource } from "@sanity/workflow-engine";
@@ -81,12 +81,13 @@ declare interface StudioUserClient {
81
81
  readonly config: () => {
82
82
  readonly projectId?: string;
83
83
  };
84
- readonly request: (options: { readonly uri: string }) => Promise<unknown>;
84
+ readonly request: (options: {
85
+ readonly uri: string;
86
+ readonly tag?: string;
87
+ }) => Promise<unknown>;
85
88
  }
86
89
 
87
- /** Native Studio profile response, whose nullable avatar is wider than Sanity's public type. */
88
- export declare interface StudioUserProfile extends Omit<User, "imageUrl"> {
89
- readonly imageUrl?: string | null;
90
+ export declare interface StudioUserProfile extends ProjectUserProfile {
90
91
  readonly [key: string]: unknown;
91
92
  }
92
93
 
@@ -173,11 +174,17 @@ export declare function useWorkflowSession({
173
174
  engine,
174
175
  instanceId,
175
176
  grantsFromPath,
177
+ guardScope,
176
178
  sdk,
177
179
  }: {
178
180
  engine: Engine;
179
181
  instanceId: string;
180
182
  grantsFromPath?: string;
183
+ /** The id-set of every co-mounted session, handed identically to each one
184
+ * so guard observation consolidates into one shared live query per
185
+ * resource. Must include `instanceId`. Omitted, guards are observed per
186
+ * instance. */
187
+ guardScope?: readonly string[];
181
188
  sdk?: SanityInstance;
182
189
  }): WorkflowSession;
183
190
 
package/dist/index.js CHANGED
@@ -6,12 +6,14 @@ import { createSanityInstance } from "@sanity/sdk";
6
6
 
7
7
  import { ENGINE_API_VERSION, createEngine, EXECUTION_KINDS } from "@sanity/workflow-engine";
8
8
 
9
- import { useMemo, useEffect, useState } from "react";
9
+ import { useMemo, useEffect, useSyncExternalStore } from "react";
10
10
 
11
11
  import { useSource, useClient } from "sanity";
12
12
 
13
13
  import { useWorkflowTelemetry, useWorkflowSession as useWorkflowSession$1, useDocumentWorkflows as useDocumentWorkflows$1, useWorkflowInstances as useWorkflowInstances$1 } from "@sanity/workflow-react";
14
14
 
15
+ import { loadProjectUserProfiles } from "@sanity/workflow-sdk/project-users";
16
+
15
17
  function makeStudioObserver(sdk, options) {
16
18
  const {client: client, engineResource: engineResource} = options;
17
19
  return {
@@ -148,7 +150,7 @@ function studioResourceClients(client) {
148
150
  }) : void 0;
149
151
  }
150
152
 
151
- function useWorkflowSession({engine: engine, instanceId: instanceId, grantsFromPath: grantsFromPath, sdk: sdk}) {
153
+ function useWorkflowSession({engine: engine, instanceId: instanceId, grantsFromPath: grantsFromPath, guardScope: guardScope, sdk: sdk}) {
152
154
  const observer = useStudioObserver({
153
155
  engine: engine,
154
156
  sdk: sdk
@@ -158,6 +160,9 @@ function useWorkflowSession({engine: engine, instanceId: instanceId, grantsFromP
158
160
  instanceId: instanceId,
159
161
  observer: observer,
160
162
  host: "studio",
163
+ ...guardScope !== void 0 ? {
164
+ guardScope: guardScope
165
+ } : {},
161
166
  opts: {
162
167
  grantsFromPath: grantsFromPath
163
168
  }
@@ -190,7 +195,7 @@ function useDocumentWorkflows({engine: engine, document: document, sdk: sdk}) {
190
195
  });
191
196
  }
192
197
 
193
- const PROJECT_USERS_TTL_MS = 3e4;
198
+ const PROJECT_USERS_RETRY_MS = 3e4;
194
199
 
195
200
  function isRecord(value) {
196
201
  return typeof value == "object" && value !== null && !Array.isArray(value);
@@ -204,10 +209,6 @@ function isMembership(value) {
204
209
  return isRecord(value) && typeof value.id == "string" && (value.isRobot === void 0 || typeof value.isRobot == "boolean") && (value.roles === void 0 || Array.isArray(value.roles) && value.roles.every(isRole));
205
210
  }
206
211
 
207
- function isUser(value) {
208
- return isRecord(value) && typeof value.id == "string" && (value.email === void 0 || typeof value.email == "string") && (value.displayName === void 0 || typeof value.displayName == "string") && (value.imageUrl === void 0 || value.imageUrl === null || typeof value.imageUrl == "string");
209
- }
210
-
211
212
  async function fetchProjectUsers(client, projectId) {
212
213
  const project = await client.request({
213
214
  uri: `/projects/${projectId}`
@@ -217,9 +218,11 @@ async function fetchProjectUsers(client, projectId) {
217
218
  if (!membershipRows.every(isMembership)) throw new Error("Studio project user response contained an invalid membership row");
218
219
  const memberships = membershipRows.filter(member => !member.isRobot);
219
220
  if (memberships.length === 0) return [];
220
- const ids = memberships.map(member => encodeURIComponent(member.id)), response = await client.request({
221
- uri: `/projects/${projectId}/users/${ids.join(",")}`
222
- }), profiles = (Array.isArray(response) ? response : [ response ]).filter(isUser), profileById = new Map(profiles.map(profile => [ profile.id, profile ]));
221
+ const profiles = await loadProjectUserProfiles({
222
+ client: client,
223
+ projectId: projectId,
224
+ ids: memberships.map(member => member.id)
225
+ }), profileById = new Map(profiles.map(profile => [ profile.id, profile ]));
223
226
  return memberships.map(membership => ({
224
227
  membership: membership,
225
228
  profile: profileById.get(membership.id)
@@ -228,6 +231,40 @@ async function fetchProjectUsers(client, projectId) {
228
231
 
229
232
  const projectUsersByClient = /* @__PURE__ */ new WeakMap;
230
233
 
234
+ function createProjectUsersStore(client, projectId) {
235
+ let snapshot = {
236
+ users: [],
237
+ loading: !1,
238
+ error: void 0
239
+ }, pending, retryAt = 0, usersById = /* @__PURE__ */ new Map, usersByEmail = /* @__PURE__ */ new Map;
240
+ const listeners = /* @__PURE__ */ new Set, setSnapshot = next => {
241
+ snapshot = next, listeners.forEach(listener => listener());
242
+ };
243
+ return {
244
+ getSnapshot: () => snapshot,
245
+ load: () => pending || (snapshot.users.length > 0 || snapshot.error === void 0 && retryAt === 1 / 0 ? Promise.resolve(snapshot.users) : snapshot.error !== void 0 && Date.now() < retryAt ? Promise.reject(snapshot.error) : (setSnapshot({
246
+ ...snapshot,
247
+ loading: !0,
248
+ error: void 0
249
+ }), pending = fetchProjectUsers(client, projectId).then(users => (usersById = new Map(users.map(user => [ user.membership.id, user ])),
250
+ usersByEmail = new Map(users.flatMap(user => user.profile?.email ? [ [ user.profile.email.toLowerCase(), user ] ] : [])),
251
+ retryAt = 1 / 0, setSnapshot({
252
+ users: users,
253
+ loading: !1,
254
+ error: void 0
255
+ }), pending = void 0, users), error => {
256
+ throw retryAt = Date.now() + PROJECT_USERS_RETRY_MS, setSnapshot({
257
+ users: [],
258
+ loading: !1,
259
+ error: error
260
+ }), pending = void 0, error;
261
+ }), pending)),
262
+ subscribe: listener => (listeners.add(listener), () => listeners.delete(listener)),
263
+ userByEmail: email => usersByEmail.get(email.toLowerCase()),
264
+ userById: id => usersById.get(id)
265
+ };
266
+ }
267
+
231
268
  function clientProjectUsers(client) {
232
269
  const cached = projectUsersByClient.get(client);
233
270
  if (cached) return cached;
@@ -235,27 +272,20 @@ function clientProjectUsers(client) {
235
272
  return projectUsersByClient.set(client, created), created;
236
273
  }
237
274
 
238
- function loadProjectUsers(client, projectId) {
275
+ function projectUsersStore(client, projectId) {
239
276
  const projectUsers = clientProjectUsers(client), cached = projectUsers.get(projectId);
240
- if (cached && cached.expiresAt > Date.now()) return cached.promise;
241
- projectUsers.delete(projectId);
242
- const pending = fetchProjectUsers(client, projectId), entry = {
243
- promise: pending,
244
- expiresAt: Number.POSITIVE_INFINITY
245
- };
246
- return projectUsers.set(projectId, entry), pending.then(() => {
247
- entry.expiresAt = Date.now() + PROJECT_USERS_TTL_MS;
248
- }, () => {
249
- projectUsers.get(projectId) === entry && projectUsers.delete(projectId);
250
- }), pending;
277
+ if (cached) return cached;
278
+ const created = createProjectUsersStore(client, projectId);
279
+ return projectUsers.set(projectId, created), created;
251
280
  }
252
281
 
253
282
  function studioProjectUserDirectory(client, projectId) {
254
- const load = () => loadProjectUsers(client, projectId);
283
+ const store = projectUsersStore(client, projectId);
255
284
  return {
256
285
  findById: async id => {
257
286
  try {
258
- const user = (await load()).find(candidate => candidate.membership.id === id);
287
+ await store.load();
288
+ const user = store.userById(id);
259
289
  return user ? {
260
290
  status: "resolved",
261
291
  user: user
@@ -271,7 +301,8 @@ function studioProjectUserDirectory(client, projectId) {
271
301
  },
272
302
  findByEmail: async email => {
273
303
  try {
274
- const normalized = email.toLowerCase(), user = (await load()).find(candidate => candidate.profile?.email?.toLowerCase() === normalized);
304
+ await store.load();
305
+ const user = store.userByEmail(email);
275
306
  return user ? {
276
307
  status: "resolved",
277
308
  user: user
@@ -288,44 +319,19 @@ function studioProjectUserDirectory(client, projectId) {
288
319
  };
289
320
  }
290
321
 
322
+ const missingProjectSnapshot = {
323
+ users: [],
324
+ loading: !1,
325
+ error: new Error("No projectId on the Studio client")
326
+ }, noopSubscribe = () => () => {};
327
+
291
328
  function useStudioProjectUsers() {
292
329
  const client = useClient({
293
330
  apiVersion: ENGINE_API_VERSION
294
- }), projectId = client.config().projectId, directory = useMemo(() => projectId ? studioProjectUserDirectory(client, projectId) : void 0, [ client, projectId ]), [state, setState] = useState({
295
- users: [],
296
- loading: projectId !== void 0,
297
- error: projectId === void 0 ? new Error("No projectId on the Studio client") : void 0
298
- });
331
+ }), projectId = client.config().projectId, directory = useMemo(() => projectId ? studioProjectUserDirectory(client, projectId) : void 0, [ client, projectId ]), store = useMemo(() => projectId ? projectUsersStore(client, projectId) : void 0, [ client, projectId ]), state = useSyncExternalStore(store?.subscribe ?? noopSubscribe, store?.getSnapshot ?? (() => missingProjectSnapshot), store?.getSnapshot ?? (() => missingProjectSnapshot));
299
332
  return useEffect(() => {
300
- if (!projectId) {
301
- setState({
302
- users: [],
303
- loading: !1,
304
- error: new Error("No projectId on the Studio client")
305
- });
306
- return;
307
- }
308
- let cancelled = !1;
309
- return setState({
310
- users: [],
311
- loading: !0,
312
- error: void 0
313
- }), loadProjectUsers(client, projectId).then(users => {
314
- cancelled || setState({
315
- users: users,
316
- loading: !1,
317
- error: void 0
318
- });
319
- }, error => {
320
- cancelled || setState({
321
- users: [],
322
- loading: !1,
323
- error: error
324
- });
325
- }), () => {
326
- cancelled = !0;
327
- };
328
- }, [ client, projectId ]), {
333
+ store && store.load().catch(() => {});
334
+ }, [ store ]), {
329
335
  ...state,
330
336
  directory: directory
331
337
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sanity/workflow-studio",
3
- "version": "0.9.0",
3
+ "version": "0.20.0",
4
4
  "description": "React adapter that drives the @sanity/workflow-engine reactive session in Sanity Studio through the App SDK store.",
5
5
  "keywords": [
6
6
  "sanity",
@@ -40,11 +40,6 @@
40
40
  "publishConfig": {
41
41
  "access": "public"
42
42
  },
43
- "dependencies": {
44
- "@sanity/workflow-engine": "0.18.0",
45
- "@sanity/workflow-react": "0.13.0",
46
- "@sanity/workflow-sdk": "0.10.0"
47
- },
48
43
  "devDependencies": {
49
44
  "@sanity/pkg-utils": "^10.5.2",
50
45
  "@sanity/sdk": "^2.12.0",
@@ -54,12 +49,18 @@
54
49
  "react": "^19.2.7",
55
50
  "react-dom": "^19.2.7",
56
51
  "sanity": "^6",
57
- "vitest": "^4.1.8"
52
+ "vitest": "^4.1.8",
53
+ "@sanity/workflow-engine": "0.20.0",
54
+ "@sanity/workflow-sdk": "0.20.0",
55
+ "@sanity/workflow-react": "0.20.0"
58
56
  },
59
57
  "peerDependencies": {
60
58
  "@sanity/sdk": "^2.12.0",
61
59
  "react": "^19.2.7",
62
- "sanity": "^6"
60
+ "sanity": "^6",
61
+ "@sanity/workflow-engine": "0.20.0",
62
+ "@sanity/workflow-react": "0.20.0",
63
+ "@sanity/workflow-sdk": "0.20.0"
63
64
  },
64
65
  "engines": {
65
66
  "node": ">=20"