@sanity/workflow-studio 0.8.0 → 0.9.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,39 @@
1
1
  # @sanity/workflow-studio
2
2
 
3
+ ## 0.9.0
4
+
5
+ ### Minor Changes
6
+
7
+ - f1fbedd: **BREAKING:** Route every Studio reactive stream through the App SDK store, removing per-document and per-query Studio listeners. `makeStudioObserver` now accepts `(sdk, {client, engineResource})`; Studio hooks bootstrap the SDK in Studio mode so both token and cookie authentication remain supported. Reactive sessions no longer include Studio-only uncommitted form-buffer edits.
8
+ - d26cc1a: Add the engine-owned Actor resolution contract with client-backed resolution for server, CLI, and MCP runtimes; add native Studio and App SDK project-user integrations; and migrate the Studio plugin member picker to the shared directory.
9
+ - f02d026: **BREAKING:** Observer stores now expose `loading`, `ready`, `invalid`, and `error` as one reactive snapshot state instead of encoding loading and invalid outcomes in the observed value. Session and instance-list hooks surface read failures through `error` and clear them when the live stream recovers. Studio reports contextual RxJS failures while retrying, and the App SDK adapter translates state-source throws into the shared contract.
10
+ - 5cd8ffd: **BREAKING:** Effect handlers now receive each host's concrete Sanity client unchanged, preserving its complete runtime and type surface. Handler traffic is no longer automatically tagged as `workflow.effect` or pinned to the engine API version; handlers own that configuration.
11
+ - 5244335: **BREAKING:** Require Sanity Studio v6. The Studio validation harness now installs the workflow CLI alongside the plugin and builds that combined package set, guarding the shared Vite 8 toolchain.
12
+
13
+ ### Patch Changes
14
+
15
+ - Updated dependencies [239d9f6]
16
+ - Updated dependencies [3af2ca0]
17
+ - Updated dependencies [7276702]
18
+ - Updated dependencies [58f8211]
19
+ - Updated dependencies [00c089d]
20
+ - Updated dependencies [71cd58e]
21
+ - Updated dependencies [cd03973]
22
+ - Updated dependencies [d26cc1a]
23
+ - Updated dependencies [9126299]
24
+ - Updated dependencies [f1fbedd]
25
+ - Updated dependencies [f02d026]
26
+ - Updated dependencies [72018af]
27
+ - Updated dependencies [5cd8ffd]
28
+ - Updated dependencies [b82bda4]
29
+ - Updated dependencies [55a54b3]
30
+ - Updated dependencies [b82bda4]
31
+ - Updated dependencies [4490442]
32
+ - Updated dependencies [1540dce]
33
+ - @sanity/workflow-engine@0.18.0
34
+ - @sanity/workflow-sdk@0.10.0
35
+ - @sanity/workflow-react@0.13.0
36
+
3
37
  ## 0.8.0
4
38
 
5
39
  ### Minor Changes
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # @sanity/workflow-studio
2
2
 
3
3
  Reactive workflow adapter for **Sanity Studio**. Everything must render inside
4
- Studio source context (`useDocumentStore` / `useSource` / `useClient`).
4
+ Studio source context (`useSource` / `useClient`).
5
5
 
6
6
  - `useWorkflowEngine({workflowResource, tag, resourceClients?})` —
7
7
  build the Studio-side engine once: the workspace client rebound to the
@@ -20,57 +20,58 @@ Studio source context (`useDocumentStore` / `useSource` / `useClient`).
20
20
  vets it.)
21
21
  - `useWorkflowSession` — drives an engine session for one instance and
22
22
  returns
23
- `{evaluation, ready, invalid, guards, tick, fireAction, editField, previewField, discardFieldPreview}`.
23
+ `{evaluation, ready, invalid, error, guards, tick, fireAction, editField, previewField, discardFieldPreview}`.
24
24
  The observer validates every engine-owned stream, so a model-ahead or
25
25
  corrupt doc surfaces as the explicit `invalid` state (never `ready` while
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}`. The list observes `documentStore.listenQuery` when
30
- the engine dataset is the workspace's own, else the App SDK store same
31
- routing (and same `sdk` rules) as the instance doc below. On multi-hook
32
- surfaces (a board with per-row `useWorkflowSession`), pass one shared
33
- `sdk` instance to the hooks each hook otherwise bootstraps its own
34
- embedded SDK instance when the engine dataset is foreign.
35
-
36
- ## How observation routes (per watched ref)
37
-
38
- Each ref in the watch-set is classified against the workspace's own
39
- project + dataset and observed by the store that can actually address it:
40
-
41
- | Ref | Observed via |
42
- | ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
43
- | **Mounted workspace dataset** | `documentStore.pair.editState` Studio's optimistic pair store, **including local uncommitted edits** (the one stream only Studio has). Non-public Studio surface — see **Private Studio seam** below |
44
- | **Engine state dataset** (instance doc) | The workspace's `documentStore` when the engine dataset IS the workspace; otherwise the **App SDK store** (`sdkInstanceDocStore` from `@sanity/workflow-sdk/observer`) |
45
- | **Guards** (`observeGuards`) | Fanned per-resource across the datasets the instance's GDRs name: mounted-dataset guards via `documentStore.listenQuery`, any foreign dataset (engine's or a subject's) via `sdkGuardStore`, merged by `combineGuardStores` |
46
- | **Any other dataset / project** | The **App SDK document store** (`sdkContentDocStore` from `@sanity/workflow-sdk/observer`) live, GDR-routed per-resource through the bootstrapped or supplied SDK instance (see below) |
47
- | **`canvas:` / `media-library:` schemes** | Routed like foreign resources through the **App SDK store**, observed live via the handle's `resource` field (`{canvasId}` / `{mediaLibraryId}`) — the same path the Canvas app uses. `dashboard:` is the exception: the App SDK store has no dashboard resource, so a `dashboard:` ref **throws** and a workflow with a dashboard subject can't be driven reactively — run it engine-direct instead |
48
-
49
- There is **no silent fallback**: a ref the configured stores can't address
50
- throws, instead of being misread from the workspace dataset under the same
51
- `_id`.
52
-
53
- - **The engine's `workflowResource` is routinely a dedicated dataset** (e.g.
54
- `workflows`), foreign to every content workspace. When it differs from the
55
- workspace and no `sdk` is supplied, the hook **bootstraps an App SDK
56
- instance automatically** from the Studio session's token, read off the
57
- source client's config (`source.getClient(...).config().token` public
58
- Studio API); that instance also observes foreign content refs. Under cookie
59
- auth the client carries no token — the hook throws an actionable error;
60
- supply your own instance: `useWorkflowSession({engine, instanceId, sdk})`.
61
- - When the engine dataset IS the workspace and a watch-set ref points at a
62
- foreign resource, there is no bootstrap signal pass `sdk` or the
63
- observer throws the config-mismatch error for that ref.
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`.
33
+ - `useStudioProjectUsers` the shared Studio project-user directory used by
34
+ actor resolution and member pickers. It returns the upstream membership and
35
+ 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.
40
+
41
+ ## How observation routes
42
+
43
+ Every reactive stream uses the App SDK store, including documents in the
44
+ mounted workspace, instance documents in the engine dataset, per-resource
45
+ guard queries, and instance lists. Document and query observation therefore
46
+ share the SDK's resource-scoped live streams instead of opening one Studio
47
+ listener per document or query.
48
+
49
+ Dataset GDRs route through `{projectId, dataset}` resource handles. `canvas:`
50
+ and `media-library:` GDRs route through `{canvasId}` and `{mediaLibraryId}`.
51
+ `dashboard:` remains unsupported because the App SDK has no dashboard document
52
+ resource; a workflow with a dashboard subject must run engine-direct.
53
+
54
+ The SDK store does not expose Studio's private, uncommitted form buffer.
55
+ Reactive evaluation follows App SDK document state; Content Lake remains the
56
+ authoritative state read by commits. For an explicit uncommitted projection,
57
+ call the session's `previewField` callback and remove it with
58
+ `discardFieldPreview`; these previews never persist the instance or content
59
+ document.
60
+
61
+ - When no `sdk` is supplied, the hook **bootstraps an App SDK instance
62
+ automatically** in Studio mode. The SDK follows Studio's reactive token
63
+ source, including token rotation; an authenticated Studio whose token source
64
+ emits `null` uses cookie authentication. Callers may instead supply their own
65
+ instance: `useWorkflowSession({engine, instanceId, sdk})`.
64
66
  - Guards (`observeGuards`) are observed wherever they live: a guard
65
67
  co-locates with the subject it locks, so the stream spans the datasets the
66
68
  instance's currently-observed GDRs name (engine dataset ∪ each
67
- open-stage/workflow-scope subject dataset). Mounted-dataset guards route
68
- through `documentStore.listenQuery`; foreign ones through the App SDK
69
- instance, the same path foreign content refs take.
69
+ open-stage/workflow-scope subject dataset). Every resource routes through
70
+ the App SDK query store.
70
71
  - **Auth caveat for foreign refs:** the SDK instance's auth must cover the
71
72
  foreign resource; a foreign _project_ needs a session/token valid there.
72
- - `makeStudioObserver(documentStore, {mounted, client, engineResource, sdk?})`
73
- is exported for custom wiring, matching `makeSdkObserver`. `client` is
73
+ - `makeStudioObserver(sdk, {client, engineResource})` is exported for custom
74
+ wiring, matching `makeSdkObserver`. `client` is
74
75
  required: a workspace-bound client that carries `ensureDocumentExists`
75
76
  writes into the mounted dataset.
76
77
 
@@ -79,29 +80,15 @@ live cross-dataset end-to-end verification) are known follow-up work.
79
80
 
80
81
  ## Packaging
81
82
 
82
- This adapter is a **superset of `@sanity/workflow-sdk`'s store layer**, not
83
- its peer. Foreign-resource observation is the routine path (the engine's
84
- state dataset is usually dedicated), and it routes through the SDK stores —
85
- so `@sanity/workflow-sdk` is a hard dependency, consumed through its headless
83
+ This adapter composes `@sanity/workflow-sdk`'s store layer through its headless
86
84
  `./observer` entry. What that means for installs:
87
85
 
88
- - **`@sanity/sdk` is required** (peer) — it addresses every resource outside
89
- the mounted workspace.
86
+ - **`@sanity/sdk` is required** (peer) — it addresses every observed resource.
90
87
  - **`@sanity/sdk-react` is NOT required** — nothing here touches the App
91
88
  SDK's React binding.
92
89
 
93
- ## Private Studio seam
94
-
95
- One stream rests on non-public Studio surface; expect Studio to be able to
96
- move it in a minor, and this package to track it:
97
-
98
- - **`documentStore.pair.editState`** (reached via `useDocumentStore`; Studio
99
- marks both `@hidden` / `@beta`) — the only stream of the optimistic pair,
100
- local uncommitted edits included. No public Studio API exposes it; until
101
- one does, this dependency is deliberate and documented rather than hidden.
102
-
103
- (The context hooks themselves — `useDocumentStore`, `useSource` — are
104
- non-public Studio surface too, the standard plugin-context caveat.)
90
+ `useSource` remains a non-public Studio context hook, which is the standard
91
+ plugin-context caveat.
105
92
 
106
93
  ## Notes
107
94
 
package/dist/index.cjs CHANGED
@@ -4,171 +4,103 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: !0
5
5
  });
6
6
 
7
- var workflowEngine = require("@sanity/workflow-engine"), observer = require("@sanity/workflow-react/observer"), observer$1 = require("@sanity/workflow-sdk/observer"), sdk = require("@sanity/sdk"), 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");
8
8
 
9
- const RESUBSCRIBE_DELAY_MS = 5e3;
10
-
11
- function observableStore({observable: observable, resolve: resolve, initial: initial, name: name}) {
12
- let latest = initial;
9
+ function makeStudioObserver(sdk2, options) {
10
+ const {client: client, engineResource: engineResource} = options;
13
11
  return {
14
- subscribe(onChange) {
15
- let subscription, retry, stopped = !1;
16
- const connect = () => {
17
- subscription = observable.subscribe({
18
- next: value => {
19
- latest = resolve(value), onChange();
20
- },
21
- error: err => {
22
- console.error(`[workflow-studio] ${name} stream errored — resubscribing in ${RESUBSCRIBE_DELAY_MS / 1e3}s:`, err),
23
- stopped || (retry = setTimeout(connect, RESUBSCRIBE_DELAY_MS));
24
- }
25
- });
26
- };
27
- return connect(), () => {
28
- stopped = !0, retry !== void 0 && clearTimeout(retry), subscription?.unsubscribe();
29
- };
30
- },
31
- getSnapshot: () => latest
32
- };
33
- }
34
-
35
- function makeStudioObserver(documentStore, options) {
36
- const {mounted: mounted, engineResource: engineResource, sdk: sdk2, client: client} = options, engineIsMounted = observer.sameDataset(engineResource, mounted), editStateStore = ({id: id, type: type, release: release, draftFallback: draftFallback}) => observableStore({
37
- observable: documentStore.pair.editState(id, type, release),
38
- resolve: editState => resolveEditState(editState, draftFallback),
39
- initial: void 0,
40
- name: `the edit state of "${id}"`
41
- }), requireSdk = what => {
42
- if (sdk2 !== void 0) return sdk2;
43
- throw new Error(`@sanity/workflow-studio: ${what} — Studio's document store is bound to ${mounted.projectId}.${mounted.dataset} and cannot observe it. Pass an App SDK instance via the hook's \`sdk\` option (e.g. \`useWorkflowSession({engine, instanceId, sdk})\`) to observe foreign resources.`);
44
- };
45
- return {
46
- observeInstance: instanceId => engineIsMounted ? observer.validatedInstanceStore(editStateStore({
47
- id: instanceId,
48
- type: workflowEngine.WORKFLOW_INSTANCE_TYPE,
49
- release: void 0,
50
- draftFallback: !0
51
- })) : observer.validatedInstanceStore(observer$1.sdkInstanceDocStore({
52
- sdk: requireSdk(`instance "${instanceId}" lives in the engine dataset ${engineResource.projectId}.${engineResource.dataset}`),
53
- instanceId: instanceId,
12
+ ...observer$1.makeSdkObserver(sdk2, {
54
13
  engineResource: engineResource
55
- })),
56
- observeDocs: (documents, perspective) => observer.combineDocStores(documents.map(ref => observer.classifyRef(ref, mounted) === "mounted-dataset" ? {
57
- key: ref.globalDocumentId,
58
- store: editStateStore({
59
- id: ref.documentId,
60
- type: ref.type,
61
- release: workflowEngine.contentReleaseName({
62
- ref: ref,
63
- perspective: perspective
64
- }),
65
- draftFallback: workflowEngine.contentDraftFallback({
66
- ref: ref,
67
- perspective: perspective
68
- })
69
- })
70
- } : {
71
- key: ref.globalDocumentId,
72
- store: observer$1.sdkContentDocStore({
73
- sdk: requireSdk(`"${ref.globalDocumentId}" lives outside this workspace`),
74
- ref: ref,
75
- perspective: perspective
76
- })
77
- })),
78
- observeGuards: (instanceId, resources) => observer.validatedGuardsStore(observer.combineGuardStores(resources.map(resource => {
79
- if (observer.sameDataset(resource, mounted)) {
80
- const {query: query, params: params} = workflowEngine.instanceGuardQuery(instanceId);
81
- return observableStore({
82
- observable: documentStore.listenQuery(query, params, {}),
83
- resolve: docs => docs,
84
- initial: void 0,
85
- name: `the guard list for "${instanceId}"`
86
- });
87
- }
88
- return observer$1.sdkGuardStore({
89
- sdk: requireSdk(`guards for "${instanceId}" live in ${resource.projectId}.${resource.dataset}`),
90
- instanceId: instanceId,
91
- resource: resource
92
- });
93
- }))),
94
- observeInstances: query => engineIsMounted ? observer.validatedInstancesStore(observableStore({
95
- observable: documentStore.listenQuery(query.query, query.params, {}),
96
- resolve: docs => docs,
97
- initial: void 0,
98
- name: "the instance list"
99
- })) : observer.validatedInstancesStore(observer$1.sdkInstancesStore({
100
- sdk: requireSdk(`the instance list lives in the engine dataset ${engineResource.projectId}.${engineResource.dataset}`),
101
- query: query,
102
- resource: engineResource
103
- })),
14
+ }),
104
15
  ensureDocumentExists: args => observer.ensureDocumentExists(client, args)
105
16
  };
106
17
  }
107
18
 
108
- function resolveEditState(editState, draftFallback) {
109
- if (!editState.ready) return;
110
- const draft = draftFallback ? editState.draft : null;
111
- return editState.version ?? draft ?? editState.published;
112
- }
113
-
114
19
  const SESSION_CLIENT_OPTIONS = {
115
20
  apiVersion: workflowEngine.ENGINE_API_VERSION
116
21
  };
117
22
 
118
23
  function useStudioObserver({engine: engine, sdk: suppliedSdk}) {
119
- const documentStore = sanity.useDocumentStore(), source = sanity.useSource(), {projectId: projectId, dataset: dataset} = source, mounted = react.useMemo(() => ({
24
+ const source = sanity.useSource(), {projectId: projectId, dataset: dataset} = source, mounted = react.useMemo(() => ({
120
25
  projectId: projectId,
121
26
  dataset: dataset
122
- }), [ projectId, dataset ]), engineResource = react.useMemo(() => observer.datasetResourceId(engine.workflowResource), [ engine ]), sdk2 = useEmbeddedSdk({
27
+ }), [ projectId, dataset ]), engineResource = react.useMemo(() => observer.datasetResourceId(engine.workflowResource), [ engine ]), studio = useStudioAuth(), sdk2 = useEmbeddedSdk({
123
28
  supplied: suppliedSdk,
124
- needed: !observer.sameDataset(engineResource, mounted),
29
+ owner: source,
125
30
  mounted: mounted,
126
- sessionToken: source.getClient(SESSION_CLIENT_OPTIONS).config().token
31
+ studio: studio
127
32
  });
128
- return react.useMemo(() => makeStudioObserver(documentStore, {
129
- mounted: mounted,
33
+ return react.useMemo(() => makeStudioObserver(sdk2, {
130
34
  client: source.getClient(SESSION_CLIENT_OPTIONS),
131
- engineResource: engineResource,
132
- ...sdk2 !== void 0 ? {
133
- sdk: sdk2
134
- } : {}
135
- }), [ documentStore, source, mounted, engineResource, sdk2 ]);
35
+ engineResource: engineResource
36
+ }), [ source, engineResource, sdk2 ]);
136
37
  }
137
38
 
138
- function useEmbeddedSdk({supplied: supplied, needed: needed, mounted: mounted, sessionToken: sessionToken}) {
139
- const wanted = supplied === void 0 && needed, held = react.useRef(void 0), key = `${mounted.projectId}.${mounted.dataset}`;
140
- wanted && (held.current?.key !== key || held.current.instance.isDisposed()) && (held.current = {
39
+ function useStudioAuth() {
40
+ const {authenticated: authenticated, auth: auth} = sanity.useSource();
41
+ return react.useMemo(() => {
42
+ const token = auth.token;
43
+ return token === void 0 ? {
44
+ authenticated: authenticated
45
+ } : {
46
+ authenticated: authenticated,
47
+ auth: {
48
+ token: token
49
+ }
50
+ };
51
+ }, [ authenticated, auth.token ]);
52
+ }
53
+
54
+ function useEmbeddedSdk({supplied: supplied, owner: owner, mounted: mounted, studio: studio}) {
55
+ const resolved = supplied === void 0 ? {
56
+ kind: "shared",
57
+ shared: sharedEmbeddedSdk({
58
+ owner: owner,
59
+ mounted: mounted,
60
+ studio: studio
61
+ })
62
+ } : {
63
+ kind: "supplied",
64
+ instance: supplied
65
+ };
66
+ return useSharedDispose(owner, resolved.kind === "shared" ? resolved.shared : void 0),
67
+ resolved.kind === "shared" ? resolved.shared.instance : resolved.instance;
68
+ }
69
+
70
+ const sharedSdks = /* @__PURE__ */ new WeakMap;
71
+
72
+ function sharedEmbeddedSdk({owner: owner, mounted: mounted, studio: studio}) {
73
+ const key = `${mounted.projectId}.${mounted.dataset}`;
74
+ let byResource = sharedSdks.get(owner);
75
+ byResource === void 0 && (byResource = /* @__PURE__ */ new Map, sharedSdks.set(owner, byResource));
76
+ const current = byResource.get(key);
77
+ if (current !== void 0 && !current.instance.isDisposed()) return current;
78
+ const shared = {
79
+ instance: bootstrapInstance(mounted, studio),
141
80
  key: key,
142
- instance: bootstrapInstance(mounted, sessionToken)
143
- });
144
- const embedded = wanted ? held.current?.instance : void 0;
145
- return useDeferredDispose(embedded), supplied ?? embedded;
81
+ users: 0
82
+ };
83
+ return byResource.set(key, shared), shared;
146
84
  }
147
85
 
148
- function bootstrapInstance(mounted, sessionToken) {
149
- if (sessionToken === void 0) throw new Error("@sanity/workflow-studio: the engine's state dataset is not this workspace's dataset, and no Studio session token is readable to bootstrap an App SDK instance (cookie-authenticated studio?). Pass one explicitly via the hook's `sdk` option, e.g. useWorkflowSession({engine, instanceId, sdk}).");
86
+ function bootstrapInstance(mounted, studio) {
150
87
  return sdk.createSanityInstance({
151
88
  projectId: mounted.projectId,
152
89
  dataset: mounted.dataset,
153
- auth: {
154
- token: sessionToken
155
- }
90
+ studio: studio
156
91
  });
157
92
  }
158
93
 
159
- function useDeferredDispose(instance) {
160
- const disposal = react.useRef(void 0);
94
+ function useSharedDispose(owner, shared) {
161
95
  react.useEffect(() => {
162
- if (instance !== void 0) return disposal.current?.instance === instance && (clearTimeout(disposal.current.timeoutId),
163
- disposal.current = void 0), () => {
164
- disposal.current = {
165
- instance: instance,
166
- timeoutId: setTimeout(() => {
167
- instance.isDisposed() || instance.dispose();
168
- }, 0)
169
- };
96
+ if (shared !== void 0) return shared.users += 1, () => {
97
+ const byResource = sharedSdks.get(owner);
98
+ byResource?.get(shared.key) === shared && (shared.users -= 1, setTimeout(() => {
99
+ shared.users !== 0 || byResource.get(shared.key) !== shared || (shared.instance.isDisposed() || shared.instance.dispose(),
100
+ byResource.delete(shared.key), byResource.size === 0 && sharedSdks.delete(owner));
101
+ }, 0));
170
102
  };
171
- }, [ instance ]);
103
+ }, [ owner, shared ]);
172
104
  }
173
105
 
174
106
  function useWorkflowEngine({workflowResource: workflowResource, tag: tag, resourceClients: resourceClients, effectHandlers: effectHandlers, missingHandler: missingHandler}) {
@@ -252,14 +184,159 @@ function useDocumentWorkflows({engine: engine, document: document, sdk: sdk2}) {
252
184
  });
253
185
  }
254
186
 
187
+ const PROJECT_USERS_TTL_MS = 3e4;
188
+
189
+ function isRecord(value) {
190
+ return typeof value == "object" && value !== null && !Array.isArray(value);
191
+ }
192
+
193
+ function isRole(value) {
194
+ return isRecord(value) && typeof value.name == "string";
195
+ }
196
+
197
+ function isMembership(value) {
198
+ 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
+ }
200
+
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
+ async function fetchProjectUsers(client, projectId) {
206
+ const project = await client.request({
207
+ uri: `/projects/${projectId}`
208
+ });
209
+ if (!isRecord(project) || !Array.isArray(project.members)) throw new Error("Studio project user response did not contain a members array");
210
+ const membershipRows = project.members;
211
+ if (!membershipRows.every(isMembership)) throw new Error("Studio project user response contained an invalid membership row");
212
+ const memberships = membershipRows.filter(member => !member.isRobot);
213
+ 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 ]));
217
+ return memberships.map(membership => ({
218
+ membership: membership,
219
+ profile: profileById.get(membership.id)
220
+ }));
221
+ }
222
+
223
+ const projectUsersByClient = /* @__PURE__ */ new WeakMap;
224
+
225
+ function clientProjectUsers(client) {
226
+ const cached = projectUsersByClient.get(client);
227
+ if (cached) return cached;
228
+ const created = /* @__PURE__ */ new Map;
229
+ return projectUsersByClient.set(client, created), created;
230
+ }
231
+
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;
245
+ }
246
+
247
+ function studioProjectUserDirectory(client, projectId) {
248
+ const load = () => loadProjectUsers(client, projectId);
249
+ return {
250
+ findById: async id => {
251
+ try {
252
+ const user = (await load()).find(candidate => candidate.membership.id === id);
253
+ return user ? {
254
+ status: "resolved",
255
+ user: user
256
+ } : {
257
+ status: "missing"
258
+ };
259
+ } catch (cause) {
260
+ return {
261
+ status: "inaccessible",
262
+ cause: cause
263
+ };
264
+ }
265
+ },
266
+ findByEmail: async email => {
267
+ try {
268
+ const normalized = email.toLowerCase(), user = (await load()).find(candidate => candidate.profile?.email?.toLowerCase() === normalized);
269
+ return user ? {
270
+ status: "resolved",
271
+ user: user
272
+ } : {
273
+ status: "missing"
274
+ };
275
+ } catch (cause) {
276
+ return {
277
+ status: "inaccessible",
278
+ cause: cause
279
+ };
280
+ }
281
+ }
282
+ };
283
+ }
284
+
285
+ function useStudioProjectUsers() {
286
+ const client = sanity.useClient({
287
+ 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
+ });
293
+ 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 ]), {
323
+ ...state,
324
+ directory: directory
325
+ };
326
+ }
327
+
255
328
  exports.makeStudioObserver = makeStudioObserver;
256
329
 
330
+ exports.studioProjectUserDirectory = studioProjectUserDirectory;
331
+
257
332
  exports.studioResourceClients = studioResourceClients;
258
333
 
259
334
  exports.useDocumentWorkflows = useDocumentWorkflows;
260
335
 
261
336
  exports.useStudioObserver = useStudioObserver;
262
337
 
338
+ exports.useStudioProjectUsers = useStudioProjectUsers;
339
+
263
340
  exports.useWorkflowEngine = useWorkflowEngine;
264
341
 
265
342
  exports.useWorkflowInstances = useWorkflowInstances;