@jskit-ai/workspaces-web 0.1.30 → 0.1.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,7 +2,6 @@ import {
2
2
  findWorkspaceBySlug,
3
3
  normalizeWorkspaceList
4
4
  } from "../lib/bootstrap.js";
5
- import { resolvePlacementUserFromBootstrapPayload } from "@jskit-ai/users-web/client/lib/bootstrap";
6
5
  import { normalizePermissionList } from "../lib/permissions.js";
7
6
  import {
8
7
  persistBootstrapThemePreference,
@@ -22,25 +21,20 @@ import {
22
21
  import {
23
22
  countPendingInvites,
24
23
  createProviderLogger,
25
- fetchBootstrapPayload,
26
24
  normalizeWorkspaceBootstrapStatus,
27
25
  normalizeWorkspaceSlugKey,
28
- resolveAuthSignature,
29
26
  resolveRequestedWorkspaceBootstrapStatus,
30
27
  resolveRouteState
31
28
  } from "./bootstrapPlacementRuntimeHelpers.js";
32
29
  import { resolveErrorStatusCode } from "../support/runtimeNormalization.js";
33
30
 
34
- function createBootstrapPlacementRuntime({ app, logger = null, fetchBootstrap = fetchBootstrapPayload } = {}) {
31
+ function createBootstrapPlacementRuntime({ app, logger = null } = {}) {
35
32
  if (!app || typeof app.has !== "function" || typeof app.make !== "function") {
36
33
  throw new Error("createBootstrapPlacementRuntime requires application has()/make().");
37
34
  }
38
35
  if (!app.has("runtime.web-placement.client")) {
39
36
  throw new Error("createBootstrapPlacementRuntime requires shell-web placement runtime.");
40
37
  }
41
- if (typeof fetchBootstrap !== "function") {
42
- throw new TypeError("createBootstrapPlacementRuntime requires fetchBootstrap(workspaceSlug).");
43
- }
44
38
 
45
39
  const runtimeLogger = logger || createProviderLogger(app);
46
40
  const placementRuntime = app.make("runtime.web-placement.client");
@@ -50,10 +44,9 @@ function createBootstrapPlacementRuntime({ app, logger = null, fetchBootstrap =
50
44
  );
51
45
  const socket = app.has("runtime.realtime.client.socket") ? app.make("runtime.realtime.client.socket") : null;
52
46
  const cleanup = [];
53
- let refreshQueue = Promise.resolve();
54
47
  let shutdownRequested = false;
55
- let authSignature = resolveAuthSignature(placementRuntime.getContext());
56
48
  let lastRouteWorkspaceSlug = resolveRouteState(placementRuntime, router).workspaceSlug;
49
+ let initialized = false;
57
50
  const workspaceBootstrapStatusBySlug = new Map();
58
51
  const workspaceBootstrapStatusListeners = new Set();
59
52
  const root = typeof globalThis === "object" && globalThis ? globalThis : null;
@@ -69,6 +62,23 @@ function createBootstrapPlacementRuntime({ app, logger = null, fetchBootstrap =
69
62
  routeGuards.shutdown();
70
63
  });
71
64
 
65
+ function normalizeBootstrapSource(source = "") {
66
+ return String(source || "workspaces-web.bootstrap-placement").trim() || "workspaces-web.bootstrap-placement";
67
+ }
68
+
69
+ function shouldApplyWorkspaceColorForContextUpdate(event = {}) {
70
+ if (event?.type !== "context.updated") {
71
+ return false;
72
+ }
73
+
74
+ const source = String(event?.source || "").trim().toLowerCase();
75
+ if (!source) {
76
+ return false;
77
+ }
78
+
79
+ return source.startsWith("workspaces-web.") && !source.startsWith("workspaces-web.bootstrap-placement");
80
+ }
81
+
72
82
  function setWorkspaceBootstrapStatus(workspaceSlug = "", status = "", source = "workspaces-web.bootstrap-placement") {
73
83
  const workspaceSlugKey = normalizeWorkspaceSlugKey(workspaceSlug);
74
84
  const normalizedStatus = normalizeWorkspaceBootstrapStatus(status);
@@ -94,7 +104,7 @@ function createBootstrapPlacementRuntime({ app, logger = null, fetchBootstrap =
94
104
  const payload = Object.freeze({
95
105
  workspaceSlug: workspaceSlugKey,
96
106
  status: normalizedStatus,
97
- source: String(source || "workspaces-web.bootstrap-placement").trim() || "workspaces-web.bootstrap-placement"
107
+ source: normalizeBootstrapSource(source)
98
108
  });
99
109
  for (const listener of workspaceBootstrapStatusListeners) {
100
110
  try {
@@ -122,7 +132,7 @@ function createBootstrapPlacementRuntime({ app, logger = null, fetchBootstrap =
122
132
  };
123
133
  }
124
134
 
125
- function writePlacementContext(payload = {}, state = {}, source = "workspaces-web.bootstrap-placement") {
135
+ function writeWorkspacePlacementContext(payload = {}, state = {}, source = "workspaces-web.bootstrap-placement") {
126
136
  const availableWorkspaces = normalizeWorkspaceList(payload?.workspaces);
127
137
  const currentWorkspace = findWorkspaceBySlug(availableWorkspaces, state.workspaceSlug);
128
138
  const workspaceSettings =
@@ -130,7 +140,6 @@ function createBootstrapPlacementRuntime({ app, logger = null, fetchBootstrap =
130
140
  ? payload.workspaceSettings
131
141
  : null;
132
142
  const permissions = normalizePermissionList(payload?.permissions);
133
- const user = resolvePlacementUserFromBootstrapPayload(payload, state.context?.user);
134
143
  const workspaceInvitesEnabled = payload?.app?.features?.workspaceInvites === true;
135
144
  const pendingInvitesCount = workspaceInvitesEnabled ? countPendingInvites(payload?.pendingInvites) : 0;
136
145
 
@@ -140,8 +149,6 @@ function createBootstrapPlacementRuntime({ app, logger = null, fetchBootstrap =
140
149
  workspaceSettings,
141
150
  workspaces: availableWorkspaces,
142
151
  permissions,
143
- user,
144
- surfaceAccess: payload?.surfaceAccess && typeof payload.surfaceAccess === "object" ? payload.surfaceAccess : {},
145
152
  pendingInvitesCount,
146
153
  workspaceInvitesEnabled
147
154
  },
@@ -150,18 +157,15 @@ function createBootstrapPlacementRuntime({ app, logger = null, fetchBootstrap =
150
157
  }
151
158
  );
152
159
  routeGuards.enforceSurfaceAccessForCurrentRoute();
153
- applyWorkspaceColorFromPlacementContext("write");
154
160
  }
155
161
 
156
- function clearPlacementContext(source = "workspaces-web.bootstrap-placement") {
162
+ function clearWorkspacePlacementContext(source = "workspaces-web.bootstrap-placement") {
157
163
  placementRuntime.setContext(
158
164
  {
159
165
  workspace: null,
160
166
  workspaceSettings: null,
161
167
  workspaces: [],
162
168
  permissions: [],
163
- user: null,
164
- surfaceAccess: {},
165
169
  pendingInvitesCount: 0,
166
170
  workspaceInvitesEnabled: false
167
171
  },
@@ -252,148 +256,195 @@ function createBootstrapPlacementRuntime({ app, logger = null, fetchBootstrap =
252
256
  }
253
257
  }
254
258
 
255
- async function refresh(reason = "manual") {
256
- if (shutdownRequested) {
257
- return;
259
+ function applyUnauthenticatedTheme(reason = "manual") {
260
+ applyThemeFromBootstrapPayload(
261
+ {
262
+ session: {
263
+ authenticated: false
264
+ }
265
+ },
266
+ reason
267
+ );
268
+ }
269
+
270
+ function normalizeBootstrapRequestContext(request = {}) {
271
+ const meta = request?.meta && typeof request.meta === "object" ? request.meta : {};
272
+ const query = request?.query && typeof request.query === "object" ? request.query : {};
273
+ return Object.freeze({
274
+ path: String(meta.path || "").trim(),
275
+ workspaceSlug: normalizeWorkspaceSlugKey(meta.workspaceSlug || query.workspaceSlug)
276
+ });
277
+ }
278
+
279
+ function isCurrentRequestTarget(request = {}) {
280
+ const requested = normalizeBootstrapRequestContext(request);
281
+ const currentState = resolveRouteState(placementRuntime, router);
282
+ if (requested.path && requested.path !== currentState.path) {
283
+ return false;
258
284
  }
285
+ return requested.workspaceSlug === normalizeWorkspaceSlugKey(currentState.workspaceSlug);
286
+ }
259
287
 
260
- const stateAtStart = resolveRouteState(placementRuntime, router);
261
- const source = `workspaces-web.bootstrap-placement.${String(reason || "manual").trim() || "manual"}`;
262
- try {
263
- const payload = await fetchBootstrap(stateAtStart.workspaceSlug);
264
- const stateAtApply = resolveRouteState(placementRuntime, router);
265
- if (stateAtStart.path !== stateAtApply.path || stateAtStart.workspaceSlug !== stateAtApply.workspaceSlug) {
266
- return;
267
- }
288
+ function resolveBootstrapRequest() {
289
+ const state = resolveRouteState(placementRuntime, router);
290
+ const workspaceSlug = normalizeWorkspaceSlugKey(state.workspaceSlug);
291
+ return Object.freeze({
292
+ query: workspaceSlug ? Object.freeze({ workspaceSlug }) : Object.freeze({}),
293
+ meta: Object.freeze({
294
+ path: state.path,
295
+ workspaceSlug
296
+ })
297
+ });
298
+ }
268
299
 
269
- writePlacementContext(payload, stateAtStart, source);
270
- applyThemeFromBootstrapPayload(payload, reason);
271
- applyWorkspaceColorFromPlacementContext(reason);
272
- if (stateAtStart.workspaceSlug) {
273
- const sessionAuthenticated = payload?.session?.authenticated === true;
274
- if (!sessionAuthenticated) {
275
- setWorkspaceBootstrapStatus(stateAtStart.workspaceSlug, WORKSPACE_BOOTSTRAP_STATUS_UNAUTHENTICATED, source);
276
- return;
277
- }
300
+ async function applyBootstrapPayload({ payload = {}, reason = "manual", source = "", request = {} } = {}) {
301
+ if (!isCurrentRequestTarget(request)) {
302
+ return null;
303
+ }
278
304
 
279
- const requestedWorkspaceStatus = resolveRequestedWorkspaceBootstrapStatus(payload, stateAtStart.workspaceSlug);
280
- if (requestedWorkspaceStatus) {
281
- setWorkspaceBootstrapStatus(stateAtStart.workspaceSlug, requestedWorkspaceStatus, source);
282
- return;
283
- }
305
+ const stateAtApply = resolveRouteState(placementRuntime, router);
306
+ const normalizedSource = normalizeBootstrapSource(source);
307
+ writeWorkspacePlacementContext(payload, stateAtApply, normalizedSource);
308
+ applyThemeFromBootstrapPayload(payload, reason);
309
+ applyWorkspaceColorFromPlacementContext(reason);
310
+ if (!stateAtApply.workspaceSlug) {
311
+ return payload;
312
+ }
284
313
 
285
- const availableWorkspaces = normalizeWorkspaceList(payload?.workspaces);
286
- const currentWorkspace = findWorkspaceBySlug(availableWorkspaces, stateAtStart.workspaceSlug);
287
- setWorkspaceBootstrapStatus(
288
- stateAtStart.workspaceSlug,
289
- currentWorkspace ? WORKSPACE_BOOTSTRAP_STATUS_RESOLVED : WORKSPACE_BOOTSTRAP_STATUS_FORBIDDEN,
290
- source
291
- );
292
- }
293
- } catch (error) {
294
- const statusCode = resolveErrorStatusCode(error);
295
- const stateAtApply = resolveRouteState(placementRuntime, router);
296
- const sameWorkspaceRoute =
297
- stateAtStart.path === stateAtApply.path && stateAtStart.workspaceSlug === stateAtApply.workspaceSlug;
298
-
299
- if (statusCode === 401) {
300
- if (stateAtStart.workspaceSlug) {
301
- setWorkspaceBootstrapStatus(stateAtStart.workspaceSlug, WORKSPACE_BOOTSTRAP_STATUS_UNAUTHENTICATED, source);
302
- }
303
- clearPlacementContext(source);
304
- applyThemeFromBootstrapPayload(
305
- {
306
- session: {
307
- authenticated: false
308
- }
309
- },
310
- reason
311
- );
312
- return;
314
+ const sessionAuthenticated = payload?.session?.authenticated === true;
315
+ if (!sessionAuthenticated) {
316
+ setWorkspaceBootstrapStatus(stateAtApply.workspaceSlug, WORKSPACE_BOOTSTRAP_STATUS_UNAUTHENTICATED, normalizedSource);
317
+ return payload;
318
+ }
319
+
320
+ const requestedWorkspaceStatus = resolveRequestedWorkspaceBootstrapStatus(payload, stateAtApply.workspaceSlug);
321
+ if (requestedWorkspaceStatus) {
322
+ setWorkspaceBootstrapStatus(stateAtApply.workspaceSlug, requestedWorkspaceStatus, normalizedSource);
323
+ return payload;
324
+ }
325
+
326
+ const availableWorkspaces = normalizeWorkspaceList(payload?.workspaces);
327
+ const currentWorkspace = findWorkspaceBySlug(availableWorkspaces, stateAtApply.workspaceSlug);
328
+ setWorkspaceBootstrapStatus(
329
+ stateAtApply.workspaceSlug,
330
+ currentWorkspace ? WORKSPACE_BOOTSTRAP_STATUS_RESOLVED : WORKSPACE_BOOTSTRAP_STATUS_FORBIDDEN,
331
+ normalizedSource
332
+ );
333
+ return payload;
334
+ }
335
+
336
+ async function handleBootstrapError({ error, reason = "manual", source = "", request = {} } = {}) {
337
+ if (!isCurrentRequestTarget(request)) {
338
+ return null;
339
+ }
340
+
341
+ const normalizedSource = normalizeBootstrapSource(source);
342
+ const requested = normalizeBootstrapRequestContext(request);
343
+ const stateAtApply = resolveRouteState(placementRuntime, router);
344
+ const statusCode = resolveErrorStatusCode(error);
345
+
346
+ if (statusCode === 401) {
347
+ if (requested.workspaceSlug) {
348
+ setWorkspaceBootstrapStatus(requested.workspaceSlug, WORKSPACE_BOOTSTRAP_STATUS_UNAUTHENTICATED, normalizedSource);
313
349
  }
314
- if (statusCode === 403) {
315
- if (stateAtStart.workspaceSlug) {
316
- setWorkspaceBootstrapStatus(stateAtStart.workspaceSlug, WORKSPACE_BOOTSTRAP_STATUS_FORBIDDEN, source);
317
- }
318
- if (sameWorkspaceRoute) {
319
- clearPlacementContext(source);
320
- }
321
- return;
350
+ clearWorkspacePlacementContext(normalizedSource);
351
+ applyUnauthenticatedTheme(reason);
352
+ return null;
353
+ }
354
+
355
+ if (statusCode === 403) {
356
+ if (requested.workspaceSlug) {
357
+ setWorkspaceBootstrapStatus(requested.workspaceSlug, WORKSPACE_BOOTSTRAP_STATUS_FORBIDDEN, normalizedSource);
322
358
  }
323
- if (statusCode === 404) {
324
- if (stateAtStart.workspaceSlug) {
325
- setWorkspaceBootstrapStatus(stateAtStart.workspaceSlug, WORKSPACE_BOOTSTRAP_STATUS_NOT_FOUND, source);
326
- }
327
- if (sameWorkspaceRoute) {
328
- clearPlacementContext(source);
329
- }
330
- return;
359
+ if (requested.path === stateAtApply.path) {
360
+ clearWorkspacePlacementContext(normalizedSource);
331
361
  }
362
+ return null;
363
+ }
332
364
 
333
- if (stateAtStart.workspaceSlug) {
334
- setWorkspaceBootstrapStatus(stateAtStart.workspaceSlug, WORKSPACE_BOOTSTRAP_STATUS_ERROR, source);
365
+ if (statusCode === 404) {
366
+ if (requested.workspaceSlug) {
367
+ setWorkspaceBootstrapStatus(requested.workspaceSlug, WORKSPACE_BOOTSTRAP_STATUS_NOT_FOUND, normalizedSource);
335
368
  }
369
+ if (requested.path === stateAtApply.path) {
370
+ clearWorkspacePlacementContext(normalizedSource);
371
+ }
372
+ return null;
373
+ }
336
374
 
337
- runtimeLogger.warn(
338
- {
339
- reason,
340
- error: String(error?.message || error || "unknown error")
341
- },
342
- "workspaces-web bootstrap placement refresh failed."
343
- );
375
+ if (requested.workspaceSlug) {
376
+ setWorkspaceBootstrapStatus(requested.workspaceSlug, WORKSPACE_BOOTSTRAP_STATUS_ERROR, normalizedSource);
344
377
  }
378
+
379
+ runtimeLogger.warn(
380
+ {
381
+ reason,
382
+ error: String(error?.message || error || "unknown error")
383
+ },
384
+ "workspaces-web bootstrap placement refresh failed."
385
+ );
386
+ return null;
345
387
  }
346
388
 
347
- function queueRefresh(reason = "manual") {
348
- refreshQueue = refreshQueue
349
- .then(() => refresh(reason))
350
- .catch((error) => {
351
- runtimeLogger.warn(
352
- {
353
- reason,
354
- error: String(error?.message || error || "unknown error")
355
- },
356
- "workspaces-web bootstrap placement queued refresh failed."
357
- );
358
- });
359
- return refreshQueue;
389
+ function resolveBootstrapRuntime() {
390
+ if (!app.has("runtime.web-bootstrap.client")) {
391
+ throw new Error("createBootstrapPlacementRuntime requires shell-web bootstrap runtime.");
392
+ }
393
+
394
+ const bootstrapRuntime = app.make("runtime.web-bootstrap.client");
395
+ if (!bootstrapRuntime || typeof bootstrapRuntime.refresh !== "function") {
396
+ throw new Error("createBootstrapPlacementRuntime requires runtime.web-bootstrap.client.refresh().");
397
+ }
398
+
399
+ return bootstrapRuntime;
400
+ }
401
+
402
+ function refresh(reason = "manual") {
403
+ if (shutdownRequested) {
404
+ return Promise.resolve(null);
405
+ }
406
+
407
+ return resolveBootstrapRuntime().refresh(reason);
408
+ }
409
+
410
+ function enforceCurrentWorkspaceStatus() {
411
+ const state = resolveRouteState(placementRuntime, router);
412
+ if (!state.workspaceSlug) {
413
+ return;
414
+ }
415
+
416
+ const status = getWorkspaceBootstrapStatus(state.workspaceSlug);
417
+ if (!status) {
418
+ return;
419
+ }
420
+
421
+ routeGuards.enforceWorkspaceRouteForStatusUpdate({
422
+ workspaceSlug: state.workspaceSlug,
423
+ status
424
+ });
360
425
  }
361
426
 
362
427
  async function initialize() {
428
+ if (initialized) {
429
+ return null;
430
+ }
431
+ initialized = true;
363
432
  routeGuards.installWorkspaceGuardEvaluator();
364
433
 
365
434
  const contextAtInit = placementRuntime.getContext();
366
435
  if (contextAtInit?.auth?.authenticated !== true) {
367
- applyThemeFromBootstrapPayload({
368
- session: {
369
- authenticated: false
370
- }
371
- }, "init");
436
+ applyUnauthenticatedTheme("init");
372
437
  }
373
438
  applyWorkspaceColorFromPlacementContext("init");
439
+ routeGuards.enforceSurfaceAccessForCurrentRoute();
440
+ enforceCurrentWorkspaceStatus();
374
441
 
375
442
  if (typeof placementRuntime.subscribe === "function") {
376
443
  const unsubscribePlacement = placementRuntime.subscribe((event = {}) => {
377
- if (event.type !== "context.updated") {
444
+ if (!shouldApplyWorkspaceColorForContextUpdate(event)) {
378
445
  return;
379
446
  }
380
-
381
- const nextContext = placementRuntime.getContext();
382
447
  applyWorkspaceColorFromPlacementContext("context");
383
- const nextSignature = resolveAuthSignature(nextContext);
384
- if (nextSignature === authSignature) {
385
- return;
386
- }
387
-
388
- authSignature = nextSignature;
389
- if (nextContext?.auth?.authenticated !== true) {
390
- applyThemeFromBootstrapPayload({
391
- session: {
392
- authenticated: false
393
- }
394
- }, "auth");
395
- }
396
- void queueRefresh("auth");
397
448
  });
398
449
  cleanup.push(() => {
399
450
  if (typeof unsubscribePlacement === "function") {
@@ -402,16 +453,17 @@ function createBootstrapPlacementRuntime({ app, logger = null, fetchBootstrap =
402
453
  });
403
454
  }
404
455
 
405
- await queueRefresh("init");
406
-
407
456
  if (router && typeof router.afterEach === "function") {
408
457
  const removeAfterEach = router.afterEach(() => {
458
+ routeGuards.enforceSurfaceAccessForCurrentRoute();
459
+ applyWorkspaceColorFromPlacementContext("route");
460
+ enforceCurrentWorkspaceStatus();
409
461
  const nextWorkspaceSlug = resolveRouteState(placementRuntime, router).workspaceSlug;
410
462
  if (nextWorkspaceSlug === lastRouteWorkspaceSlug) {
411
463
  return;
412
464
  }
413
465
  lastRouteWorkspaceSlug = nextWorkspaceSlug;
414
- void queueRefresh("route");
466
+ void refresh("route");
415
467
  });
416
468
  cleanup.push(() => {
417
469
  if (typeof removeAfterEach === "function") {
@@ -422,7 +474,7 @@ function createBootstrapPlacementRuntime({ app, logger = null, fetchBootstrap =
422
474
 
423
475
  if (socket && typeof socket.on === "function") {
424
476
  const handleBootstrapChanged = () => {
425
- void queueRefresh("realtime");
477
+ void refresh("realtime");
426
478
  };
427
479
  socket.on("users.bootstrap.changed", handleBootstrapChanged);
428
480
  cleanup.push(() => {
@@ -447,7 +499,10 @@ function createBootstrapPlacementRuntime({ app, logger = null, fetchBootstrap =
447
499
  return Object.freeze({
448
500
  initialize,
449
501
  shutdown,
450
- refresh: queueRefresh,
502
+ refresh,
503
+ resolveBootstrapRequest,
504
+ applyBootstrapPayload,
505
+ handleBootstrapError,
451
506
  getWorkspaceBootstrapStatus,
452
507
  subscribeWorkspaceBootstrapStatus
453
508
  });
@@ -1,3 +1,4 @@
1
+ import { unref } from "vue";
1
2
  import { getClientAppConfig } from "@jskit-ai/kernel/client";
2
3
  import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
3
4
  import {
@@ -6,18 +7,10 @@ import {
6
7
 
7
8
  const WORKSPACES_WEB_SCOPE_SUPPORT_INJECTION_KEY = "jskit.workspaces.web.scope-support";
8
9
 
9
- function unwrapRefValue(value) {
10
- if (value && typeof value === "object" && Object.hasOwn(value, "value")) {
11
- return value.value;
12
- }
13
-
14
- return value;
15
- }
16
-
17
10
  function readWorkspaceRouteScope(routeContext = {}) {
18
- const placementContext = unwrapRefValue(routeContext?.placementContext);
19
- const currentSurfaceId = normalizeText(unwrapRefValue(routeContext?.currentSurfaceId)).toLowerCase();
20
- const routePath = normalizeText(unwrapRefValue(routeContext?.routePath));
11
+ const placementContext = unref(routeContext?.placementContext);
12
+ const currentSurfaceId = normalizeText(unref(routeContext?.currentSurfaceId)).toLowerCase();
13
+ const routePath = normalizeText(unref(routeContext?.routePath));
21
14
  const workspaceSlug = extractWorkspaceSlugFromSurfacePathname(
22
15
  placementContext,
23
16
  currentSurfaceId,