@sanity/workbench 0.1.0-alpha.19 → 0.1.0-alpha.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/system.js CHANGED
@@ -1,10 +1,117 @@
1
- import { getAuthState, logout, AuthStateType, getClient, createSanityInstance } from "@sanity/sdk";
2
- import { fromObservable, fromPromise, setup, assign, sendParent, fromCallback, sendTo, raise } from "xstate";
3
- import "./_chunks-es/studio.js";
1
+ import { fromPromise, setup, assign, sendParent, fromObservable, fromCallback, enqueueActions, sendTo, raise } from "xstate";
4
2
  import { logger } from "./_chunks-es/index.js";
5
3
  import { createInstance } from "@sanity/federation/runtime";
6
4
  import { log } from "@sanity/federation/runtime/plugins/log";
5
+ import { getAuthState, logout, AuthStateType, getClient, createSanityInstance } from "@sanity/sdk";
6
+ import "./_chunks-es/studio.js";
7
7
  import { createSessionId, createBatchedStore } from "@sanity/telemetry";
8
+ async function loadRemoteModule(instance, { moduleId, entry }) {
9
+ const name = moduleId.slice(0, moduleId.indexOf("/"));
10
+ return instance.registerRemotes([{ name, entry }]), await instance.loadRemote(moduleId) ?? null;
11
+ }
12
+ async function loadFederatedModule(instance, ref) {
13
+ try {
14
+ return await loadRemoteModule(instance, ref);
15
+ } catch (error) {
16
+ return logger.error(`Failed to load federated module "${ref.moduleId}"`, error), null;
17
+ }
18
+ }
19
+ class ModuleShapeError extends Error {
20
+ constructor(remoteId) {
21
+ super(`Remote "${remoteId}" did not expose a render function`);
22
+ }
23
+ }
24
+ class ModuleLoadError extends Error {
25
+ constructor(remoteId) {
26
+ super(`Remote module "${remoteId}" failed to load`);
27
+ }
28
+ }
29
+ const loadLogic = fromPromise(async ({ input }) => {
30
+ const remoteModule = await loadRemoteModule(input.instance, {
31
+ moduleId: input.moduleId,
32
+ entry: input.entry
33
+ });
34
+ if (remoteModule == null)
35
+ throw new ModuleLoadError(input.moduleId);
36
+ return remoteModule;
37
+ }), remoteLogic = setup({
38
+ types: {
39
+ input: {},
40
+ context: {},
41
+ tags: {}
42
+ },
43
+ actors: {
44
+ load: loadLogic
45
+ },
46
+ actions: {
47
+ setModule: assign({
48
+ module: (_, params) => params.module,
49
+ error: () => null
50
+ }),
51
+ setError: assign({
52
+ module: () => null,
53
+ error: (_, params) => normaliseError(params.error)
54
+ })
55
+ }
56
+ }).createMachine({
57
+ id: "remote",
58
+ initial: "loading",
59
+ context: ({ input }) => ({
60
+ ...input,
61
+ module: null,
62
+ error: null
63
+ }),
64
+ states: {
65
+ loading: {
66
+ tags: ["loading"],
67
+ invoke: {
68
+ src: "load",
69
+ input: ({ context }) => ({
70
+ moduleId: context.moduleId,
71
+ entry: context.entry,
72
+ instance: context.instance
73
+ }),
74
+ onDone: {
75
+ target: "loaded",
76
+ actions: [
77
+ {
78
+ type: "setModule",
79
+ params: ({ event }) => ({ module: event.output })
80
+ }
81
+ ]
82
+ },
83
+ onError: {
84
+ target: "error",
85
+ actions: [
86
+ {
87
+ type: "setError",
88
+ params: ({ event }) => ({ error: event.error })
89
+ }
90
+ ]
91
+ }
92
+ }
93
+ },
94
+ loaded: {
95
+ tags: ["ready"],
96
+ entry: sendParent(({ context }) => ({
97
+ type: "remote.settled",
98
+ moduleId: context.moduleId,
99
+ status: "loaded"
100
+ }))
101
+ },
102
+ error: {
103
+ tags: ["failed"],
104
+ entry: sendParent(({ context }) => ({
105
+ type: "remote.settled",
106
+ moduleId: context.moduleId,
107
+ status: "error"
108
+ }))
109
+ }
110
+ }
111
+ });
112
+ function normaliseError(error) {
113
+ return error instanceof Error ? { message: error.message, cause: error } : { message: String(error), cause: error };
114
+ }
8
115
  const authStateLogic = fromObservable(
9
116
  ({ input }) => getAuthState(input.instance).observable
10
117
  ), logoutActorLogic = fromPromise(async ({ input }) => {
@@ -200,153 +307,236 @@ const authStateLogic = fromObservable(
200
307
  log2.debug("action", event.action);
201
308
  break;
202
309
  }
203
- };
204
- class ModuleShapeError extends Error {
205
- constructor(remoteId) {
206
- super(`Remote "${remoteId}" did not expose a render function`);
207
- }
208
- }
209
- const REMOTE_MODULE = "App", loadLogic = fromPromise(
210
- async ({ input }) => {
211
- input.instance.registerRemotes([{ name: input.id, entry: input.entry }]);
212
- const remoteModule = await input.instance.loadRemote(
213
- `${input.id}/${REMOTE_MODULE}`
214
- );
215
- if (!remoteModule || typeof remoteModule.render != "function")
216
- throw new ModuleShapeError(input.id);
217
- return remoteModule;
218
- }
219
- ), remoteLogic = setup({
310
+ }, remotesLogic = setup({
220
311
  types: {
221
312
  input: {},
222
313
  context: {},
223
- tags: {}
314
+ events: {}
224
315
  },
225
316
  actors: {
226
- load: loadLogic
317
+ remote: remoteLogic
318
+ },
319
+ guards: {
320
+ remoteNotKnown: ({ context }, params) => !context.children.has(params.moduleId)
227
321
  },
228
322
  actions: {
229
- setModule: assign({
230
- module: (_, params) => params.module,
231
- error: () => null
232
- }),
233
- setError: assign({
234
- module: () => null,
235
- error: (_, params) => normaliseError(params.error)
323
+ spawnRemote: assign({
324
+ children: ({ context, spawn }, params) => {
325
+ const ref = spawn("remote", {
326
+ id: params.moduleId,
327
+ systemId: `remotes.${params.moduleId}`,
328
+ input: {
329
+ moduleId: params.moduleId,
330
+ entry: params.entry,
331
+ instance: context.instance
332
+ }
333
+ });
334
+ return new Map(context.children).set(params.moduleId, ref);
335
+ }
236
336
  })
237
337
  }
238
338
  }).createMachine({
239
- id: "remote",
240
- initial: "loading",
339
+ id: "remotes",
241
340
  context: ({ input }) => ({
242
- ...input,
243
- module: null,
244
- error: null
341
+ instance: input.instance,
342
+ children: /* @__PURE__ */ new Map()
245
343
  }),
344
+ on: {
345
+ "remote.load.request": {
346
+ guard: {
347
+ type: "remoteNotKnown",
348
+ params: ({ event }) => ({ moduleId: event.moduleId })
349
+ },
350
+ actions: [
351
+ {
352
+ type: "spawnRemote",
353
+ params: ({ event }) => ({
354
+ moduleId: event.moduleId,
355
+ entry: event.entry
356
+ })
357
+ }
358
+ ]
359
+ },
360
+ // Reserved for future supervision/retry. Forwarded by per-remote
361
+ // children on entry to `loaded` or `error`; currently a no-op so the
362
+ // event is part of the typed surface rather than a silent unknown.
363
+ "remote.settled": {}
364
+ }
365
+ }), SERVICES_MODULE = "services", runWorker = fromCallback(({ input }) => {
366
+ let worker, blobUrl;
367
+ try {
368
+ const bootstrap = `import(${JSON.stringify(input.workerUrl)})`;
369
+ blobUrl = URL.createObjectURL(
370
+ new Blob([bootstrap], { type: "text/javascript" })
371
+ ), worker = new Worker(blobUrl, { type: "module" }), worker.addEventListener("message", (event) => {
372
+ if (event.data?.kind === "workbench.worker.error")
373
+ logger.error(
374
+ `Service "${input.key}" worker error`,
375
+ event.data.payload?.message
376
+ );
377
+ else if (event.data?.kind === "workbench.worker.log") {
378
+ const { level, message } = event.data.payload ?? {};
379
+ ({
380
+ warn: logger.warn,
381
+ error: logger.error,
382
+ debug: logger.debug
383
+ }[level] ?? logger.info)(
384
+ `[service:${input.serviceName}] ${message ?? ""}`
385
+ );
386
+ }
387
+ }), worker.addEventListener("error", (event) => {
388
+ logger.error(
389
+ `Service "${input.key}" worker error: ${event.message || String(event)}`
390
+ );
391
+ });
392
+ } catch (error) {
393
+ logger.error(`Service "${input.key}" failed to start its worker`, error);
394
+ }
395
+ return () => {
396
+ try {
397
+ worker?.postMessage({ kind: "workbench.worker.terminate" });
398
+ } finally {
399
+ worker?.terminate(), blobUrl && URL.revokeObjectURL(blobUrl);
400
+ }
401
+ };
402
+ }), serviceLogic = setup({
403
+ types: {
404
+ input: {},
405
+ context: {},
406
+ tags: {}
407
+ },
408
+ actors: {
409
+ loadInterface: remoteLogic,
410
+ runWorker
411
+ }
412
+ }).createMachine({
413
+ id: "service",
414
+ initial: "loading",
415
+ context: ({ input }) => ({ ...input, workerUrl: null }),
416
+ // The invoked loader `sendParent`s a `remote.settled` on settle; this machine
417
+ // reacts via `onSnapshot` instead, so the event is a no-op here.
418
+ on: { "remote.settled": {} },
246
419
  states: {
247
420
  loading: {
248
421
  tags: ["loading"],
249
422
  invoke: {
250
- src: "load",
423
+ id: "loader",
424
+ src: "loadInterface",
251
425
  input: ({ context }) => ({
252
- id: context.id,
426
+ moduleId: `${context.appId}/${SERVICES_MODULE}/${context.serviceName}`,
253
427
  entry: context.entry,
254
428
  instance: context.instance
255
429
  }),
256
- onDone: {
257
- target: "loaded",
258
- actions: [
259
- {
260
- type: "setModule",
261
- params: ({ event }) => ({ module: event.output })
262
- }
263
- ]
264
- },
265
- onError: {
266
- target: "error",
267
- actions: [
268
- {
269
- type: "setError",
270
- params: ({ event }) => ({ error: event.error })
430
+ onSnapshot: [
431
+ {
432
+ // Loaded and exposing a bundle URL → run it.
433
+ guard: ({ event }) => {
434
+ const module = event.snapshot.context.module;
435
+ return event.snapshot.hasTag("ready") && typeof module?.url == "string";
436
+ },
437
+ target: "running",
438
+ actions: assign({
439
+ workerUrl: ({ context, event }) => {
440
+ const module = event.snapshot.context.module;
441
+ return new URL(module.url, new URL(context.entry).origin).href;
442
+ }
443
+ })
444
+ },
445
+ {
446
+ // Load failed, or loaded without a usable URL → no worker.
447
+ guard: ({ event }) => {
448
+ const module = event.snapshot.context.module;
449
+ return event.snapshot.hasTag("failed") || event.snapshot.hasTag("ready") && typeof module?.url != "string";
450
+ },
451
+ target: "error",
452
+ // A worker has no UI surface (unlike a view's error boundary), so a
453
+ // failed load would otherwise be silent — log it through the host.
454
+ actions: ({ context, event }) => {
455
+ const cause = event.snapshot.context.error?.message;
456
+ logger.error(
457
+ `Service "${context.key}" failed to load its worker${cause ? `: ${cause}` : ""}`
458
+ );
271
459
  }
272
- ]
273
- }
460
+ }
461
+ ]
274
462
  }
275
463
  },
276
- loaded: {
277
- tags: ["ready"],
278
- entry: sendParent(({ context }) => ({
279
- type: "remote.settled",
280
- id: context.id,
281
- status: "loaded"
282
- }))
464
+ running: {
465
+ tags: ["running"],
466
+ invoke: {
467
+ src: "runWorker",
468
+ input: ({ context }) => ({
469
+ key: context.key,
470
+ serviceName: context.serviceName,
471
+ // `workerUrl` is set on entry to `running` from `loading`'s output.
472
+ workerUrl: context.workerUrl
473
+ })
474
+ }
283
475
  },
284
476
  error: {
285
- tags: ["failed"],
286
- entry: sendParent(({ context }) => ({
287
- type: "remote.settled",
288
- id: context.id,
289
- status: "error"
290
- }))
477
+ tags: ["failed"]
291
478
  }
292
479
  }
293
480
  });
294
- function normaliseError(error) {
295
- return error instanceof Error ? { message: error.message, cause: error } : { message: String(error), cause: error };
481
+ function serviceKey(appId, serviceName) {
482
+ return `${appId}:${serviceName}`;
296
483
  }
297
- const remotesLogic = setup({
484
+ const servicesLogic = setup({
298
485
  types: {
486
+ input: {},
299
487
  context: {},
300
488
  events: {}
301
489
  },
302
490
  actors: {
303
- remote: remoteLogic
304
- },
305
- guards: {
306
- remoteNotKnown: ({ context }, params) => !context.children.has(params.id)
491
+ service: serviceLogic
307
492
  },
308
493
  actions: {
309
- spawnRemote: assign({
310
- children: ({ context, spawn }, params) => {
311
- const ref = spawn("remote", {
312
- id: params.id,
313
- systemId: `remotes.${params.id}`,
314
- input: {
315
- id: params.id,
316
- entry: params.entry,
317
- instance: context.instance
318
- }
319
- });
320
- return new Map(context.children).set(params.id, ref);
321
- }
494
+ /**
495
+ * Reconcile the running workers against the desired set: spawn the ones not
496
+ * yet running, stop (and forget) the ones no longer declared.
497
+ */
498
+ syncServices: enqueueActions(({ context, event, enqueue }) => {
499
+ const desired = new Map(
500
+ event.services.map((service) => [
501
+ serviceKey(service.appId, service.serviceName),
502
+ service
503
+ ])
504
+ );
505
+ for (const [key, ref] of context.children)
506
+ desired.has(key) || enqueue.stopChild(ref);
507
+ enqueue.assign({
508
+ children: ({ context: ctx, spawn }) => {
509
+ const next = /* @__PURE__ */ new Map();
510
+ for (const [key, ref] of ctx.children)
511
+ desired.has(key) && next.set(key, ref);
512
+ for (const [key, service] of desired)
513
+ next.has(key) || next.set(
514
+ key,
515
+ spawn("service", {
516
+ id: key,
517
+ systemId: `services.${key}`,
518
+ input: {
519
+ key,
520
+ appId: service.appId,
521
+ serviceName: service.serviceName,
522
+ entry: service.entry,
523
+ instance: ctx.instance
524
+ }
525
+ })
526
+ );
527
+ return next;
528
+ }
529
+ });
322
530
  })
323
531
  }
324
532
  }).createMachine({
325
- id: "remotes",
326
- context: () => ({
327
- instance: createInstance({
328
- name: "workbench-applications",
329
- plugins: [log(logger.debug)]
330
- }),
533
+ id: "services",
534
+ context: ({ input }) => ({
535
+ instance: input.instance,
331
536
  children: /* @__PURE__ */ new Map()
332
537
  }),
333
538
  on: {
334
- "remote.load.request": {
335
- guard: {
336
- type: "remoteNotKnown",
337
- params: ({ event }) => ({ id: event.id })
338
- },
339
- actions: [
340
- {
341
- type: "spawnRemote",
342
- params: ({ event }) => ({ id: event.id, entry: event.entry })
343
- }
344
- ]
345
- },
346
- // Reserved for future supervision/retry. Forwarded by per-remote
347
- // children on entry to `loaded` or `error`; currently a no-op so the
348
- // event is part of the typed surface rather than a silent unknown.
349
- "remote.settled": {}
539
+ "services.sync": { actions: "syncServices" }
350
540
  }
351
541
  }), DEFAULT_PREFERRED_COLOR_SCHEME = "system", DEFAULT_OS_COLOR_SCHEME = "light", resolveColorScheme = (preferred, osColorScheme) => preferred === "system" ? osColorScheme : preferred, adapterBridgeLogic = fromCallback(({ input: adapter, sendBack, receive }) => {
352
542
  if (!adapter) return;
@@ -555,7 +745,8 @@ const remotesLogic = setup({
555
745
  auth: authLogic,
556
746
  telemetry: telemetryLogic,
557
747
  systemPreferences: systemPreferencesLogic,
558
- remotes: remotesLogic
748
+ remotes: remotesLogic,
749
+ services: servicesLogic
559
750
  },
560
751
  guards: {
561
752
  hasTag: (_, params) => params.hasTag
@@ -574,6 +765,10 @@ const remotesLogic = setup({
574
765
  id: "os",
575
766
  context: ({ input }) => ({
576
767
  instance: createSanityInstance(),
768
+ federationInstance: createInstance({
769
+ name: "workbench-applications",
770
+ plugins: [log(logger.debug)]
771
+ }),
577
772
  userProperties: {
578
773
  version: input.version,
579
774
  organizationId: input.organizationId,
@@ -639,7 +834,14 @@ const remotesLogic = setup({
639
834
  {
640
835
  id: "remotes",
641
836
  systemId: "remotes",
642
- src: "remotes"
837
+ src: "remotes",
838
+ input: ({ context }) => ({ instance: context.federationInstance })
839
+ },
840
+ {
841
+ id: "services",
842
+ systemId: "services",
843
+ src: "services",
844
+ input: ({ context }) => ({ instance: context.federationInstance })
643
845
  }
644
846
  ],
645
847
  states: {
@@ -678,7 +880,9 @@ function createOSOptions(input) {
678
880
  };
679
881
  }
680
882
  export {
883
+ ModuleShapeError,
681
884
  createOSOptions,
885
+ loadFederatedModule,
682
886
  os
683
887
  };
684
888
  //# sourceMappingURL=system.js.map