@sanity/workflow-studio 0.7.1 → 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,65 @@
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
+
37
+ ## 0.8.0
38
+
39
+ ### Minor Changes
40
+
41
+ - 5a1a9fe: The observers validate every engine-owned stream (instance doc, guards, instance lists) at the store boundary — the engine's read discipline (data-model gate + shape parse) instead of a cast, with a failure surfaced as the session's explicit `invalid` state. Content-doc streams are foreign and pass through unvalidated, as before.
42
+
43
+ ### Patch Changes
44
+
45
+ - Updated dependencies [f9389e5]
46
+ - Updated dependencies [d0c62ea]
47
+ - Updated dependencies [c3eed2e]
48
+ - Updated dependencies [df4bd80]
49
+ - Updated dependencies [092a0d4]
50
+ - Updated dependencies [5a1a9fe]
51
+ - Updated dependencies [5a1a9fe]
52
+ - Updated dependencies [5a1a9fe]
53
+ - Updated dependencies [e3a7ba2]
54
+ - Updated dependencies [30fed9e]
55
+ - Updated dependencies [c3eed2e]
56
+ - Updated dependencies [1321ba5]
57
+ - Updated dependencies [e683875]
58
+ - Updated dependencies [a8ace4d]
59
+ - @sanity/workflow-engine@0.17.0
60
+ - @sanity/workflow-sdk@0.9.0
61
+ - @sanity/workflow-react@0.12.0
62
+
3
63
  ## 0.7.1
4
64
 
5
65
  ### Patch 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
@@ -10,58 +10,68 @@ Studio source context (`useDocumentStore` / `useSource` / `useClient`).
10
10
  exported) as the default per-resource routing that reaches any `dataset:`
11
11
  GDR by rebinding the same client, pinned to the engine version too. A custom `resourceClients` replaces the default — delegate
12
12
  to `studioResourceClients` for the dataset arm; schemes it returns
13
- `undefined` for (media-library, canvas) are routed by the engine itself,
13
+ `undefined` for (media-library, canvas) are READ by the engine itself,
14
14
  which derives a sibling client from the engine client's credentials.
15
+ Writing a runtime-supplied ref to such a scheme into field state
16
+ additionally needs the resolver to SERVE that resource — serving is what
17
+ declares it on the engine's written-ref surface; an undeclared
18
+ media-library/canvas ref is rejected at the write with
19
+ `RefResourceUndeclaredError`. (Definition content is unaffected — deploy
20
+ vets it.)
15
21
  - `useWorkflowSession` — drives an engine session for one instance and
16
22
  returns
17
- `{evaluation, ready, guards, tick, fireAction, editField, previewField, discardFieldPreview}`.
23
+ `{evaluation, ready, invalid, error, guards, tick, fireAction, editField, previewField, discardFieldPreview}`.
24
+ The observer validates every engine-owned stream, so a model-ahead or
25
+ corrupt doc surfaces as the explicit `invalid` state (never `ready` while
26
+ set) instead of an eternal spinner.
18
27
  - `useWorkflowInstances` / `useDocumentWorkflows` — live instance lists
19
28
  (filterable collection / per-document discovery), each returning
20
- `{instances, loading}`. The list observes `documentStore.listenQuery` when
21
- the engine dataset is the workspace's own, else the App SDK store same
22
- routing (and same `sdk` rules) as the instance doc below. On multi-hook
23
- surfaces (a board with per-row `useWorkflowSession`), pass one shared
24
- `sdk` instance to the hooks each hook otherwise bootstraps its own
25
- embedded SDK instance when the engine dataset is foreign.
26
-
27
- ## How observation routes (per watched ref)
28
-
29
- Each ref in the watch-set is classified against the workspace's own
30
- project + dataset and observed by the store that can actually address it:
31
-
32
- | Ref | Observed via |
33
- | ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
34
- | **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 |
35
- | **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`) |
36
- | **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` |
37
- | **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) |
38
- | **`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 |
39
-
40
- There is **no silent fallback**: a ref the configured stores can't address
41
- throws, instead of being misread from the workspace dataset under the same
42
- `_id`.
43
-
44
- - **The engine's `workflowResource` is routinely a dedicated dataset** (e.g.
45
- `workflows`), foreign to every content workspace. When it differs from the
46
- workspace and no `sdk` is supplied, the hook **bootstraps an App SDK
47
- instance automatically** from the Studio session's token, read off the
48
- source client's config (`source.getClient(...).config().token` public
49
- Studio API); that instance also observes foreign content refs. Under cookie
50
- auth the client carries no token — the hook throws an actionable error;
51
- supply your own instance: `useWorkflowSession({engine, instanceId, sdk})`.
52
- - When the engine dataset IS the workspace and a watch-set ref points at a
53
- foreign resource, there is no bootstrap signal pass `sdk` or the
54
- 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})`.
55
66
  - Guards (`observeGuards`) are observed wherever they live: a guard
56
67
  co-locates with the subject it locks, so the stream spans the datasets the
57
68
  instance's currently-observed GDRs name (engine dataset ∪ each
58
- open-stage/workflow-scope subject dataset). Mounted-dataset guards route
59
- through `documentStore.listenQuery`; foreign ones through the App SDK
60
- 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.
61
71
  - **Auth caveat for foreign refs:** the SDK instance's auth must cover the
62
72
  foreign resource; a foreign _project_ needs a session/token valid there.
63
- - `makeStudioObserver(documentStore, {mounted, client, engineResource, sdk?})`
64
- is exported for custom wiring, matching `makeSdkObserver`. `client` is
73
+ - `makeStudioObserver(sdk, {client, engineResource})` is exported for custom
74
+ wiring, matching `makeSdkObserver`. `client` is
65
75
  required: a workspace-bound client that carries `ensureDocumentExists`
66
76
  writes into the mounted dataset.
67
77
 
@@ -70,29 +80,15 @@ live cross-dataset end-to-end verification) are known follow-up work.
70
80
 
71
81
  ## Packaging
72
82
 
73
- This adapter is a **superset of `@sanity/workflow-sdk`'s store layer**, not
74
- its peer. Foreign-resource observation is the routine path (the engine's
75
- state dataset is usually dedicated), and it routes through the SDK stores —
76
- 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
77
84
  `./observer` entry. What that means for installs:
78
85
 
79
- - **`@sanity/sdk` is required** (peer) — it addresses every resource outside
80
- the mounted workspace.
86
+ - **`@sanity/sdk` is required** (peer) — it addresses every observed resource.
81
87
  - **`@sanity/sdk-react` is NOT required** — nothing here touches the App
82
88
  SDK's React binding.
83
89
 
84
- ## Private Studio seam
85
-
86
- One stream rests on non-public Studio surface; expect Studio to be able to
87
- move it in a minor, and this package to track it:
88
-
89
- - **`documentStore.pair.editState`** (reached via `useDocumentStore`; Studio
90
- marks both `@hidden` / `@beta`) — the only stream of the optimistic pair,
91
- local uncommitted edits included. No public Studio API exposes it; until
92
- one does, this dependency is deliberate and documented rather than hidden.
93
-
94
- (The context hooks themselves — `useDocumentStore`, `useSource` — are
95
- 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.
96
92
 
97
93
  ## Notes
98
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 ? editStateStore({
47
- id: instanceId,
48
- type: workflowEngine.WORKFLOW_INSTANCE_TYPE,
49
- release: void 0,
50
- draftFallback: !0
51
- }) : 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
14
  }),
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.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 ? observableStore({
95
- observable: documentStore.listenQuery(query.query, query.params, {}),
96
- resolve: docs => docs,
97
- initial: void 0,
98
- name: "the instance list"
99
- }) : 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
- }),
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 ]);
37
+ }
38
+
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 ]);
136
52
  }
137
53
 
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 = {
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;