@floegence/redevplugin-ui 0.5.1 → 0.6.5

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.
@@ -1,5 +1,5 @@
1
1
  import { type FetchLike } from "./http.js";
2
- import type { components, operations } from "./openapi.gen.js";
2
+ import type { components } from "./openapi.gen.js";
3
3
  import { type PluginConfirmationHandler, type PluginSurfaceHost, type PluginSurfaceOpeningProgress, type PluginSurfaceReloadLimiter, type PluginSurfaceSlot, type PluginTrustedMethodResult, type ReDevPluginSurfaceTransport } from "./surface.js";
4
4
  import { type PluginSurfaceScope } from "./surface-scope.js";
5
5
  export type PluginPlatformClientOptions = {
@@ -46,7 +46,9 @@ export type PluginOpenSurfaceInSlotOptions = PluginRequestOptions & {
46
46
  onOpeningProgress?: (progress: PluginSurfaceOpeningProgress) => void;
47
47
  onError?: (error: import("./errors.js").PluginBridgeError) => void;
48
48
  };
49
- export type PluginSurfaceScopeRevokeResult = PlatformSchemas["SurfaceScopeRevokeResult"];
49
+ export type PluginSessionScopeRevokeCounts = Readonly<PlatformSchemas["SessionScopeRevokeCounts"]>;
50
+ export type PluginSessionScopeRevokeResult = Readonly<PlatformSchemas["SessionScopeRevokeCompleteResult"]>;
51
+ export type PluginSessionScopeState = PluginSessionScopeRevokeResult["state"];
50
52
  export type PluginSurfaceBootstrapResult = PlatformSchemas["SurfaceBootstrap"];
51
53
  export type PluginRuntimeStartRequest = PlatformSchemas["StartRuntimeRequest"];
52
54
  export type PluginRuntimeTarget = NonNullable<PluginRuntimeStartRequest["target"]>;
@@ -72,15 +74,15 @@ export type PluginCapabilityContractPin = PlatformSchemas["HostCapabilityPinV1"]
72
74
  export type PluginPublicOperationBinding = PlatformSchemas["PublicOperationBinding"];
73
75
  export type PluginOperationRecord = PlatformSchemas["OperationRecord"];
74
76
  export type PluginOperationList = PlatformSchemas["PluginOperationList"];
75
- export type PluginOperationListOptions = NonNullable<operations["listPluginOperations"]["parameters"]["query"]>;
77
+ export type PluginOperationListOptions = PlatformSchemas["ListOperationsQueryRequest"];
76
78
  export type PluginIntentRecord = PlatformSchemas["PluginIntentRecord"];
77
79
  export type PluginIntentList = PlatformSchemas["PluginIntentList"];
78
- export type PluginIntentListOptions = NonNullable<operations["listPluginIntents"]["parameters"]["query"]>;
80
+ export type PluginIntentListOptions = PlatformSchemas["ListIntentsQueryRequest"];
79
81
  export type PluginIntentInvokeRequest = PlatformSchemas["InvokeIntentRequest"];
80
82
  export type PluginPermissionGrant = PlatformSchemas["PluginPermissionGrant"];
81
83
  export type PluginPermissionMutationResult = PlatformSchemas["PluginPermissionMutationResult"];
82
84
  export type PluginPermissionList = PlatformSchemas["PluginPermissionList"];
83
- export type PluginPermissionListOptions = NonNullable<operations["listPluginPermissionGrants"]["parameters"]["query"]>;
85
+ export type PluginPermissionListOptions = PlatformSchemas["ListPermissionsQueryRequest"];
84
86
  export type PluginPermissionGrantRequest = PlatformSchemas["GrantPermissionRequest"];
85
87
  export type PluginPermissionRevokeRequest = PlatformSchemas["RevokePermissionRequest"];
86
88
  export type PluginSecurityPolicy = PlatformSchemas["PluginSecurityPolicy"];
@@ -94,7 +96,7 @@ export type PluginDataExportDeleteRequest = PlatformSchemas["DeleteDataExportReq
94
96
  export type PluginDataExportDeleteResult = PlatformSchemas["PluginDataExportDeleteResult"];
95
97
  export type PluginDataImportRequest = PlatformSchemas["ImportDataRequest"];
96
98
  export type PluginDataBinding = PlatformSchemas["PluginDataBinding"];
97
- export type PluginRetainedDataListOptions = NonNullable<operations["listPluginRetainedData"]["parameters"]["query"]>;
99
+ export type PluginRetainedDataListOptions = PlatformSchemas["ListRetainedDataQueryRequest"];
98
100
  export type PluginRetainedDataList = PlatformSchemas["RetainedDataList"];
99
101
  export type PluginRetainedDataDeleteRequest = PlatformSchemas["DeleteRetainedDataRequest"];
100
102
  export type PluginRetainedDataBindRequest = PlatformSchemas["BindRetainedDataRequest"];
@@ -108,7 +110,7 @@ export type PluginDiagnosticEvent = PlatformSchemas["PluginDiagnosticEvent"];
108
110
  export type PluginDiagnosticEventList = PlatformSchemas["PluginDiagnosticEventList"];
109
111
  export type PluginDiagnosticDetails = PlatformSchemas["PluginDiagnosticDetails"];
110
112
  export type PluginDiagnosticMutationOutcome = PlatformSchemas["DiagnosticMutationOutcome"];
111
- export type PluginDiagnosticListOptions = NonNullable<operations["listPluginDiagnosticEvents"]["parameters"]["query"]>;
113
+ export type PluginDiagnosticListOptions = PlatformSchemas["ListDiagnosticsQueryRequest"];
112
114
  export type PluginDiagnosticSeverity = NonNullable<PluginDiagnosticListOptions["severity"]>;
113
115
  export declare class PluginPlatformClient {
114
116
  #private;
@@ -123,7 +125,7 @@ export declare class PluginPlatformClient {
123
125
  disablePlugin(request: PluginDisableRequest, options?: PluginRequestOptions): Promise<PluginRecord>;
124
126
  uninstallPlugin(request: PluginUninstallRequest, options?: PluginRequestOptions): Promise<PluginRecord>;
125
127
  openSurfaceInSlot(slot: PluginSurfaceSlot, request: PluginOpenSurfaceRequest, options?: PluginOpenSurfaceInSlotOptions): Promise<PluginSurfaceHost>;
126
- revokeSurfaceScope(options?: PluginRequestOptions): Promise<PluginSurfaceScopeRevokeResult>;
128
+ revokeSessionScope(options?: PluginRequestOptions): Promise<PluginSessionScopeRevokeResult>;
127
129
  startRuntime(request: PluginRuntimeStartRequest, options?: PluginRequestOptions): Promise<PluginRuntimeHealth>;
128
130
  stopRuntime(options?: PluginRequestOptions): Promise<PluginRuntimeStopResult>;
129
131
  refreshEnabledRuntimeState(options?: PluginRequestOptions): Promise<PluginRuntimeRefreshResult>;
package/dist/platform.js CHANGED
@@ -1,7 +1,7 @@
1
- import { assertMutationDispatchable, defaultFetch, dispatchMutationRequest, readMutationPlatformResponse, readPlatformResponse, trimTrailingSlash, } from "./http.js";
1
+ import { assertMutationDispatchable, defaultFetch, dispatchMutationRequest, dispatchQueryRequest, readMutationPlatformResponse, readPlatformResponse, trimTrailingSlash, } from "./http.js";
2
2
  import { PluginBridgeError, PluginMutationLifecycleError, PluginTransportError, pluginMutationOutcome, } from "./errors.js";
3
3
  import { createReDevPluginSurfaceTransport, openPluginSurfaceInSlot, } from "./surface.js";
4
- import { defaultPluginSurfaceScope, disposePluginSurfaceScope, } from "./surface-scope.js";
4
+ import { defaultPluginSurfaceScope, disposePluginSurfaceScope, invalidatePluginSurfaceScope, } from "./surface-scope.js";
5
5
  function toPluginSurfaceHostBootstrap(value) {
6
6
  return {
7
7
  pluginId: value.plugin_id,
@@ -33,9 +33,9 @@ export class PluginPlatformClient {
33
33
  this.#surfaceTransport = options.surfaceTransport;
34
34
  this.#onMutationOutcomeUnknown = options.onMutationOutcomeUnknown;
35
35
  }
36
- catalog(options = {}) { return this.#getJSON("/_redevplugin/api/plugins/catalog", options); }
37
- features(options = {}) { return this.#getJSON("/_redevplugin/api/plugins/features", options); }
38
- getCompatibility(options = {}) { return this.#getJSON("/_redevplugin/api/plugins/platform/compatibility", options); }
36
+ catalog(options = {}) { return this.#requestQuery("/_redevplugin/api/plugins/catalog/query", {}, options); }
37
+ features(options = {}) { return this.#requestQuery("/_redevplugin/api/plugins/features/query", {}, options); }
38
+ getCompatibility(options = {}) { return this.#requestQuery("/_redevplugin/api/plugins/platform/compatibility/query", {}, options); }
39
39
  installReleaseRef(request, options = {}) {
40
40
  return this.#requestMutation("POST", "/_redevplugin/api/plugins/install-release-ref", request, options);
41
41
  }
@@ -89,16 +89,20 @@ export class PluginPlatformClient {
89
89
  })),
90
90
  });
91
91
  }
92
- async revokeSurfaceScope(options = {}) {
92
+ async revokeSessionScope(options = {}) {
93
93
  let result;
94
94
  try {
95
- result = await this.#requestMutation("POST", "/_redevplugin/api/plugins/surfaces/revoke-scope", {}, options);
95
+ const raw = await this.#requestMutation("POST", "/_redevplugin/api/plugins/session/revoke-scope", {}, options);
96
+ if (!isPluginSessionScopeRevokeResult(raw)) {
97
+ throw new PluginTransportError("Plugin session scope revocation returned an invalid result", new PluginBridgeError("PLUGIN_CONTRACT_MISMATCH", "Invalid session scope revocation result"), "unknown");
98
+ }
99
+ result = raw;
96
100
  }
97
101
  catch (error) {
98
- await this.#handleMutationFailure(error, undefined, "Plugin surface scope revocation and local teardown failed");
102
+ await this.#handleSessionRevokeFailure(error);
99
103
  throw error;
100
104
  }
101
- await disposePluginSurfaceScope(this.#surfaceScope);
105
+ await invalidatePluginSurfaceScope(this.#surfaceScope);
102
106
  return result;
103
107
  }
104
108
  startRuntime(request, options = {}) {
@@ -110,41 +114,27 @@ export class PluginPlatformClient {
110
114
  refreshEnabledRuntimeState(options = {}) {
111
115
  return this.#requestMutation("POST", "/_redevplugin/api/plugins/runtime/refresh-enabled", {}, options);
112
116
  }
113
- runtimeHealth(options = {}) { return this.#getJSON("/_redevplugin/api/plugins/runtime/health", options); }
117
+ runtimeHealth(options = {}) { return this.#requestQuery("/_redevplugin/api/plugins/runtime/health/query", {}, options); }
114
118
  getSettingsSchema(pluginInstanceId, scope, options = {}) {
115
- return this.#getJSON(`/_redevplugin/api/plugins/${encodeURIComponent(pluginInstanceId)}/settings/schema?scope=${encodeURIComponent(scope)}`, options);
119
+ return this.#requestQuery(`/_redevplugin/api/plugins/${encodeURIComponent(pluginInstanceId)}/settings/schema/query`, { scope }, options);
116
120
  }
117
121
  getSettings(pluginInstanceId, scope, options = {}) {
118
- return this.#getJSON(`/_redevplugin/api/plugins/${encodeURIComponent(pluginInstanceId)}/settings?scope=${encodeURIComponent(scope)}`, options);
122
+ return this.#requestQuery(`/_redevplugin/api/plugins/${encodeURIComponent(pluginInstanceId)}/settings/query`, { scope }, options);
119
123
  }
120
124
  patchSettings(pluginInstanceId, request, options = {}) {
121
125
  return this.#mutatePluginAt("PATCH", `/_redevplugin/api/plugins/${encodeURIComponent(pluginInstanceId)}/settings`, pluginInstanceId, request, options);
122
126
  }
123
127
  listOperations(options = {}, requestOptions = {}) {
124
- const params = new URLSearchParams();
125
- if (options.plugin_instance_id)
126
- params.set("plugin_instance_id", options.plugin_instance_id);
127
- if (options.cursor)
128
- params.set("cursor", options.cursor);
129
- if (options.limit !== undefined)
130
- params.set("limit", String(options.limit));
131
- const query = params.toString();
132
- return this.#getJSON(`/_redevplugin/api/plugins/operations${query ? `?${query}` : ""}`, requestOptions);
128
+ return this.#requestQuery("/_redevplugin/api/plugins/operations/query", options, requestOptions);
133
129
  }
134
130
  getOperation(operationId, options = {}) {
135
- return this.#getJSON(`/_redevplugin/api/plugins/operations/${encodeURIComponent(operationId)}`, options);
131
+ return this.#requestQuery(`/_redevplugin/api/plugins/operations/${encodeURIComponent(operationId)}/query`, {}, options);
136
132
  }
137
133
  cancelOperation(operationId, reason, options = {}) {
138
134
  return this.#requestMutation("POST", `/_redevplugin/api/plugins/operations/${encodeURIComponent(operationId)}/cancel`, reason ? { reason } : {}, options);
139
135
  }
140
136
  listIntents(options = {}, requestOptions = {}) {
141
- const params = new URLSearchParams();
142
- if (options.intent_id !== undefined)
143
- params.set("intent_id", options.intent_id);
144
- if (options.plugin_instance_id !== undefined)
145
- params.set("plugin_instance_id", options.plugin_instance_id);
146
- const query = params.toString();
147
- return this.#getJSON(`/_redevplugin/api/plugins/intents${query ? `?${query}` : ""}`, requestOptions);
137
+ return this.#requestQuery("/_redevplugin/api/plugins/intents/query", options, requestOptions);
148
138
  }
149
139
  invokeIntent(request, options = {}) {
150
140
  return this.#requestMutation("POST", "/_redevplugin/api/plugins/intents/invoke", request, options);
@@ -159,11 +149,7 @@ export class PluginPlatformClient {
159
149
  return this.#mutatePluginAt("POST", "/_redevplugin/api/plugins/data/import", request.plugin_instance_id, request, options);
160
150
  }
161
151
  listRetainedData(options = {}, requestOptions = {}) {
162
- const params = new URLSearchParams();
163
- if (options.plugin_instance_id)
164
- params.set("plugin_instance_id", options.plugin_instance_id);
165
- const query = params.toString();
166
- return this.#getJSON(`/_redevplugin/api/plugins/retained-data${query ? `?${query}` : ""}`, requestOptions);
152
+ return this.#requestQuery("/_redevplugin/api/plugins/retained-data/query", options, requestOptions);
167
153
  }
168
154
  deleteRetainedData(request, options = {}) {
169
155
  return this.#requestMutation("POST", "/_redevplugin/api/plugins/retained-data/delete", request, options);
@@ -175,13 +161,7 @@ export class PluginPlatformClient {
175
161
  return this.#requestMutation("POST", "/_redevplugin/api/plugins/retained-data/cleanup-expired", request, options);
176
162
  }
177
163
  listPermissions(options = {}, requestOptions = {}) {
178
- const params = new URLSearchParams();
179
- if (options.plugin_instance_id !== undefined)
180
- params.set("plugin_instance_id", options.plugin_instance_id);
181
- if (options.active_only !== undefined)
182
- params.set("active_only", options.active_only ? "true" : "false");
183
- const query = params.toString();
184
- return this.#getJSON(`/_redevplugin/api/plugins/permissions${query ? `?${query}` : ""}`, requestOptions);
164
+ return this.#requestQuery("/_redevplugin/api/plugins/permissions/query", options, requestOptions);
185
165
  }
186
166
  grantPermission(request, options = {}) {
187
167
  return this.#mutatePlugin("/_redevplugin/api/plugins/permissions/grant", request, options);
@@ -190,10 +170,10 @@ export class PluginPlatformClient {
190
170
  return this.#mutatePlugin("/_redevplugin/api/plugins/permissions/revoke", request, options);
191
171
  }
192
172
  listSecurityPolicies(options = {}) {
193
- return this.#getJSON("/_redevplugin/api/plugins/security-policies", options);
173
+ return this.#requestQuery("/_redevplugin/api/plugins/security-policies/query", {}, options);
194
174
  }
195
175
  getSecurityPolicy(pluginInstanceId, options = {}) {
196
- return this.#getJSON(`/_redevplugin/api/plugins/security-policies/${encodeURIComponent(pluginInstanceId)}`, options);
176
+ return this.#requestQuery(`/_redevplugin/api/plugins/security-policies/${encodeURIComponent(pluginInstanceId)}/query`, {}, options);
197
177
  }
198
178
  putSecurityPolicy(pluginInstanceId, request, options = {}) {
199
179
  return this.#mutatePluginAt("PUT", `/_redevplugin/api/plugins/security-policies/${encodeURIComponent(pluginInstanceId)}`, pluginInstanceId, request, options);
@@ -211,10 +191,7 @@ export class PluginPlatformClient {
211
191
  return this.#requestMutation("POST", "/_redevplugin/api/plugins/secrets/delete", request, options);
212
192
  }
213
193
  listDiagnosticEvents(options = {}, requestOptions = {}) {
214
- return this.#getJSON(`/_redevplugin/api/plugins/diagnostics${queryString(options)}`, requestOptions);
215
- }
216
- #getJSON(path, options) {
217
- return this.#requestJSON("GET", path, options);
194
+ return this.#requestQuery("/_redevplugin/api/plugins/diagnostics/query", options, requestOptions);
218
195
  }
219
196
  #mutatePlugin(path, request, options) {
220
197
  return this.#mutatePluginAt("POST", path, request.plugin_instance_id, request, options);
@@ -232,7 +209,8 @@ export class PluginPlatformClient {
232
209
  return result;
233
210
  }
234
211
  async #handleMutationFailure(error, pluginInstanceId, message) {
235
- if (pluginMutationOutcome(error) === "not_committed")
212
+ const outcome = pluginMutationOutcome(error);
213
+ if (outcome === "not_committed" || outcome === undefined)
236
214
  return;
237
215
  const lifecycleErrors = [];
238
216
  try {
@@ -241,28 +219,58 @@ export class PluginPlatformClient {
241
219
  catch (caught) {
242
220
  lifecycleErrors.push(caught);
243
221
  }
244
- try {
245
- this.#onMutationOutcomeUnknown?.(pluginInstanceId);
246
- }
247
- catch (caught) {
248
- lifecycleErrors.push(caught);
222
+ if (outcome === "unknown") {
223
+ try {
224
+ this.#onMutationOutcomeUnknown?.(pluginInstanceId);
225
+ }
226
+ catch (caught) {
227
+ lifecycleErrors.push(caught);
228
+ }
249
229
  }
250
230
  if (lifecycleErrors.length > 0)
251
231
  throw new PluginMutationLifecycleError(message, error, lifecycleErrors);
252
232
  }
253
- async #requestJSON(method, path, options) {
254
- let response;
233
+ async #handleSessionRevokeFailure(error) {
234
+ const outcome = pluginMutationOutcome(error);
235
+ if (outcome === "not_committed")
236
+ return;
237
+ const lifecycleErrors = [];
238
+ if (outcome === "committed" || outcome === "unknown") {
239
+ try {
240
+ await invalidatePluginSurfaceScope(this.#surfaceScope);
241
+ }
242
+ catch (caught) {
243
+ lifecycleErrors.push(caught);
244
+ }
245
+ }
246
+ if (outcome === "unknown") {
247
+ try {
248
+ this.#onMutationOutcomeUnknown?.();
249
+ }
250
+ catch (caught) {
251
+ lifecycleErrors.push(caught);
252
+ }
253
+ }
254
+ if (lifecycleErrors.length > 0) {
255
+ throw new PluginMutationLifecycleError("Plugin session scope revocation and local invalidation failed", error, lifecycleErrors);
256
+ }
257
+ }
258
+ async #requestQuery(path, body, options) {
259
+ const operation = `POST ${path}`;
260
+ let encodedBody;
255
261
  try {
256
- response = await this.#fetch(this.#apiBaseURL + path, {
257
- method,
258
- headers: { "Accept": "application/json" },
259
- credentials: "same-origin",
260
- signal: options.signal,
261
- });
262
+ encodedBody = JSON.stringify(body);
262
263
  }
263
264
  catch (cause) {
264
- throw new PluginTransportError(`Plugin platform request failed for ${method} ${path}`, cause);
265
+ throw new PluginTransportError(`Plugin platform query body serialization failed for ${operation}`, cause);
265
266
  }
267
+ const response = await dispatchQueryRequest(this.#fetch, this.#apiBaseURL + path, {
268
+ method: "POST",
269
+ headers: { "Accept": "application/json", "Content-Type": "application/json" },
270
+ body: encodedBody,
271
+ credentials: "same-origin",
272
+ signal: options.signal,
273
+ }, operation);
266
274
  return readPlatformResponse(response);
267
275
  }
268
276
  async #requestMutation(method, path, body, options) {
@@ -289,14 +297,38 @@ export class PluginPlatformClient {
289
297
  return readMutationPlatformResponse(response);
290
298
  }
291
299
  }
292
- function queryString(values) {
293
- const params = new URLSearchParams();
294
- for (const [key, value] of Object.entries(values)) {
295
- if (value !== undefined)
296
- params.set(key, String(value));
297
- }
298
- const query = params.toString();
299
- return query ? `?${query}` : "";
300
+ const sessionScopeCountKeys = [
301
+ "surfaces",
302
+ "asset_tickets",
303
+ "asset_sessions",
304
+ "plugin_gateway_tokens",
305
+ "confirmation_tokens",
306
+ "stream_tickets",
307
+ "handle_grants",
308
+ "confirmations",
309
+ "operations",
310
+ "streams",
311
+ "runtime_executions",
312
+ "active_network_requests",
313
+ "sockets",
314
+ "network_streams",
315
+ "storage_hostcalls",
316
+ ];
317
+ function isPluginSessionScopeRevokeResult(value) {
318
+ if (!isExactRecord(value, ["state", "fenced", "complete", "counts"]) ||
319
+ value.state !== "complete" || value.fenced !== true || value.complete !== true)
320
+ return false;
321
+ const counts = value.counts;
322
+ if (!isExactRecord(counts, sessionScopeCountKeys))
323
+ return false;
324
+ return sessionScopeCountKeys.every((key) => Number.isSafeInteger(counts[key]) && Number(counts[key]) >= 0);
325
+ }
326
+ function isExactRecord(value, keys) {
327
+ if (typeof value !== "object" || value === null || Array.isArray(value))
328
+ return false;
329
+ const actual = Object.keys(value).sort();
330
+ const expected = [...keys].sort();
331
+ return actual.length === expected.length && actual.every((key, index) => key === expected[index]);
300
332
  }
301
333
  function surfaceTransportAPIBaseURL(value) {
302
334
  if (!value || value.startsWith("/"))
@@ -4,5 +4,6 @@ export declare class PluginSurfaceScope {
4
4
  }
5
5
  export declare function createPluginSurfaceScope(): PluginSurfaceScope;
6
6
  export declare const defaultPluginSurfaceScope: PluginSurfaceScope;
7
- export declare function registerPluginSurface(scope: PluginSurfaceScope, pluginInstanceId: string, dispose: () => Promise<void> | void): () => void;
7
+ export declare function registerPluginSurface(scope: PluginSurfaceScope, pluginInstanceId: string, dispose: () => Promise<void> | void, invalidate: () => Promise<void> | void): () => void;
8
8
  export declare function disposePluginSurfaceScope(scope: PluginSurfaceScope, pluginInstanceId?: string): Promise<void>;
9
+ export declare function invalidatePluginSurfaceScope(scope: PluginSurfaceScope): Promise<void>;
@@ -1,4 +1,4 @@
1
- const registrations = new WeakMap();
1
+ const scopes = new WeakMap();
2
2
  function canonicalPluginInstanceId(pluginInstanceId) {
3
3
  const canonical = typeof pluginInstanceId === "string" ? pluginInstanceId.trim() : "";
4
4
  if (!canonical)
@@ -7,7 +7,7 @@ function canonicalPluginInstanceId(pluginInstanceId) {
7
7
  }
8
8
  export class PluginSurfaceScope {
9
9
  constructor() {
10
- registrations.set(this, new Map());
10
+ scopes.set(this, { invalidated: false, registrations: new Map() });
11
11
  }
12
12
  static create() {
13
13
  return new PluginSurfaceScope();
@@ -17,25 +17,34 @@ export function createPluginSurfaceScope() {
17
17
  return PluginSurfaceScope.create();
18
18
  }
19
19
  export const defaultPluginSurfaceScope = createPluginSurfaceScope();
20
- export function registerPluginSurface(scope, pluginInstanceId, dispose) {
21
- const state = registrations.get(scope);
20
+ export function registerPluginSurface(scope, pluginInstanceId, dispose, invalidate) {
21
+ const state = scopes.get(scope);
22
22
  if (!state)
23
23
  throw new TypeError("Plugin surface scope is invalid");
24
24
  const canonicalPluginId = canonicalPluginInstanceId(pluginInstanceId);
25
25
  const registration = Symbol(canonicalPluginId);
26
- state.set(registration, { pluginInstanceId: canonicalPluginId, dispose });
27
- return () => state.delete(registration);
26
+ if (state.invalidated) {
27
+ try {
28
+ void Promise.resolve(invalidate()).catch(() => undefined);
29
+ }
30
+ catch {
31
+ // A fenced session remains invalid even when a local observer fails.
32
+ }
33
+ return () => undefined;
34
+ }
35
+ state.registrations.set(registration, { pluginInstanceId: canonicalPluginId, dispose, invalidate });
36
+ return () => state.registrations.delete(registration);
28
37
  }
29
38
  export async function disposePluginSurfaceScope(scope, pluginInstanceId) {
30
- const state = registrations.get(scope);
39
+ const state = scopes.get(scope);
31
40
  if (!state)
32
41
  throw new TypeError("Plugin surface scope is invalid");
33
42
  const canonicalPluginId = pluginInstanceId === undefined
34
43
  ? undefined
35
44
  : canonicalPluginInstanceId(pluginInstanceId);
36
- const selected = [...state.entries()].filter(([, registration]) => canonicalPluginId === undefined || registration.pluginInstanceId === canonicalPluginId);
45
+ const selected = [...state.registrations.entries()].filter(([, registration]) => canonicalPluginId === undefined || registration.pluginInstanceId === canonicalPluginId);
37
46
  for (const [key] of selected)
38
- state.delete(key);
47
+ state.registrations.delete(key);
39
48
  const results = await Promise.allSettled(selected.map(([, registration]) => Promise.resolve().then(() => registration.dispose())));
40
49
  const failures = results
41
50
  .filter((result) => result.status === "rejected")
@@ -45,3 +54,19 @@ export async function disposePluginSurfaceScope(scope, pluginInstanceId) {
45
54
  if (failures.length > 1)
46
55
  throw new AggregateError(failures, "Plugin surface scope teardown failed");
47
56
  }
57
+ export async function invalidatePluginSurfaceScope(scope) {
58
+ const state = scopes.get(scope);
59
+ if (!state)
60
+ throw new TypeError("Plugin surface scope is invalid");
61
+ state.invalidated = true;
62
+ const selected = [...state.registrations.values()];
63
+ state.registrations.clear();
64
+ const results = await Promise.allSettled(selected.map((registration) => Promise.resolve().then(() => registration.invalidate())));
65
+ const failures = results
66
+ .filter((result) => result.status === "rejected")
67
+ .map((result) => result.reason);
68
+ if (failures.length === 1)
69
+ throw failures[0];
70
+ if (failures.length > 1)
71
+ throw new AggregateError(failures, "Plugin surface scope invalidation failed");
72
+ }
package/dist/surface.js CHANGED
@@ -2289,6 +2289,7 @@ class PluginSurfaceHostImplementation {
2289
2289
  #closePromise;
2290
2290
  #revokePromise;
2291
2291
  #unregisterSurfaceScope;
2292
+ #onLocalInvalidation;
2292
2293
  #opened = false;
2293
2294
  #ready = false;
2294
2295
  #disposed = false;
@@ -2325,7 +2326,7 @@ class PluginSurfaceHostImplementation {
2325
2326
  this.#onOpeningProgress = options.onOpeningProgress;
2326
2327
  this.#onError = options.onError;
2327
2328
  hardenPluginSurfaceFrame(this.#iframe);
2328
- this.#unregisterSurfaceScope = registerPluginSurface(options.surfaceScope ?? defaultPluginSurfaceScope, this.bootstrap.pluginInstanceId, () => this.#disposeLocal());
2329
+ this.#unregisterSurfaceScope = registerPluginSurface(options.surfaceScope ?? defaultPluginSurfaceScope, this.bootstrap.pluginInstanceId, () => this.#disposeLocal(), () => this.#invalidateLocal());
2329
2330
  }
2330
2331
  async open() {
2331
2332
  this.#assertActive();
@@ -2447,6 +2448,11 @@ class PluginSurfaceHostImplementation {
2447
2448
  this.#disposeLocal();
2448
2449
  return revoke;
2449
2450
  }
2451
+ observeLocalInvalidation(observer) {
2452
+ this.#onLocalInvalidation = observer;
2453
+ if (this.#disposed)
2454
+ observer();
2455
+ }
2450
2456
  async #closeSurface() {
2451
2457
  const startedAt = performance.now();
2452
2458
  const quiesce = await this.#quiesceSurface();
@@ -2512,6 +2518,16 @@ class PluginSurfaceHostImplementation {
2512
2518
  }
2513
2519
  this.#iframe.srcdoc = "";
2514
2520
  }
2521
+ #invalidateLocal() {
2522
+ this.#disposeLocal();
2523
+ this.#iframe.remove?.();
2524
+ try {
2525
+ this.#onLocalInvalidation?.();
2526
+ }
2527
+ catch {
2528
+ // Slot state observers cannot weaken local session containment.
2529
+ }
2530
+ }
2515
2531
  async #handlePortMessage(event) {
2516
2532
  if (this.#disposed || !messageWithinLimit(event.data))
2517
2533
  return;
@@ -3201,6 +3217,7 @@ function createPluginSurfaceOpeningLease(request) {
3201
3217
  let unregister = () => undefined;
3202
3218
  let revokePromise;
3203
3219
  let removeAbortListener = () => undefined;
3220
+ let invalidationObserver = () => undefined;
3204
3221
  const lease = {
3205
3222
  options: Promise.resolve(undefined),
3206
3223
  pluginInstanceId: request.pluginInstanceId,
@@ -3222,7 +3239,23 @@ function createPluginSurfaceOpeningLease(request) {
3222
3239
  return Promise.resolve();
3223
3240
  return lease.revoke();
3224
3241
  },
3242
+ invalidate() {
3243
+ lease.cancelled = true;
3244
+ if (lease.owner === "host" || lease.owner === "invalidated")
3245
+ return;
3246
+ lease.owner = "invalidated";
3247
+ unregister();
3248
+ removeAbortListener();
3249
+ invalidationObserver();
3250
+ },
3251
+ observeInvalidation(observer) {
3252
+ invalidationObserver = observer;
3253
+ if (lease.owner === "invalidated")
3254
+ observer();
3255
+ },
3225
3256
  revoke() {
3257
+ if (lease.owner === "invalidated")
3258
+ return Promise.resolve();
3226
3259
  if (lease.owner === "host") {
3227
3260
  throw new PluginBridgeError("PLUGIN_CONTRACT_MISMATCH", "Plugin surface ownership was already adopted");
3228
3261
  }
@@ -3236,12 +3269,17 @@ function createPluginSurfaceOpeningLease(request) {
3236
3269
  return revokePromise;
3237
3270
  },
3238
3271
  };
3239
- unregister = registerPluginSurface(request.surfaceScope, request.pluginInstanceId, () => lease.cancel());
3240
- try {
3241
- lease.options = Promise.resolve(request.open());
3272
+ unregister = registerPluginSurface(request.surfaceScope, request.pluginInstanceId, () => lease.cancel(), () => lease.invalidate());
3273
+ if (lease.owner === "invalidated") {
3274
+ lease.options = Promise.reject(new PluginBridgeError("PLUGIN_BRIDGE_DISPOSED", "Plugin surface scope is invalidated"));
3242
3275
  }
3243
- catch (error) {
3244
- lease.options = Promise.reject(error);
3276
+ else {
3277
+ try {
3278
+ lease.options = Promise.resolve(request.open());
3279
+ }
3280
+ catch (error) {
3281
+ lease.options = Promise.reject(error);
3282
+ }
3245
3283
  }
3246
3284
  void lease.options.catch(() => {
3247
3285
  if (lease.owner !== "opening")
@@ -3301,6 +3339,12 @@ export class PluginSurfaceSlot {
3301
3339
  const controller = new AbortController();
3302
3340
  this.#transitionController = controller;
3303
3341
  this.#setState("opening");
3342
+ lease?.observeInvalidation(() => {
3343
+ if (this.#transitionController !== controller)
3344
+ return;
3345
+ controller.abort();
3346
+ this.#setState("empty");
3347
+ });
3304
3348
  const transition = this.#tail.then(() => this.#openSurface(resolvedOptions, controller, lease), async (error) => {
3305
3349
  await this.#revokeOpening(lease);
3306
3350
  throw error;
@@ -3319,9 +3363,12 @@ export class PluginSurfaceSlot {
3319
3363
  resolvedOptions = await options;
3320
3364
  }
3321
3365
  catch (error) {
3322
- if (!this.#disposed && this.#transitionController === controller && !controller.signal.aborted) {
3366
+ if (!this.#disposed && this.#transitionController === controller && !this.#openingCancelled(controller, lease)) {
3323
3367
  this.#setState("error", toBridgeError(error, "PLUGIN_BRIDGE_HANDSHAKE_FAILED"));
3324
3368
  }
3369
+ else {
3370
+ this.#setCancelledState(controller);
3371
+ }
3325
3372
  throw error;
3326
3373
  }
3327
3374
  if (lease && resolvedOptions.bootstrap.pluginInstanceId !== lease.pluginInstanceId) {
@@ -3360,6 +3407,17 @@ export class PluginSurfaceSlot {
3360
3407
  }
3361
3408
  throw error;
3362
3409
  }
3410
+ host.observeLocalInvalidation(() => {
3411
+ if (this.#active === host)
3412
+ this.#active = undefined;
3413
+ if (this.#opening === host)
3414
+ this.#opening = undefined;
3415
+ if (this.#transitionController === controller)
3416
+ controller.abort();
3417
+ host.element.remove();
3418
+ if (!this.#disposed)
3419
+ this.#setState("empty");
3420
+ });
3363
3421
  this.#adoptOpening(lease);
3364
3422
  this.#opening = host;
3365
3423
  setSurfaceInteractive(host.element, false);
@@ -3788,7 +3846,8 @@ function isBridgeResponse(value) {
3788
3846
  if (value.ok !== false || typeof value.error_code !== "string" || !pluginBridgeErrorCodeSet.has(value.error_code) ||
3789
3847
  typeof value.error !== "string" || value.error.length > 4096 ||
3790
3848
  !Object.keys(value).every((key) => ["type", "id", "ok", "error_code", "error", "error_details", "mutation_outcome"].includes(key)) ||
3791
- (value.mutation_outcome !== undefined && value.mutation_outcome !== "not_committed" && value.mutation_outcome !== "unknown")) {
3849
+ (value.mutation_outcome !== undefined && value.mutation_outcome !== "committed" &&
3850
+ value.mutation_outcome !== "not_committed" && value.mutation_outcome !== "unknown")) {
3792
3851
  return false;
3793
3852
  }
3794
3853
  if (value.error_code === "PLUGIN_CAPABILITY_ERROR")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@floegence/redevplugin-ui",
3
- "version": "0.5.1",
3
+ "version": "0.6.5",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {
@@ -31,6 +31,9 @@
31
31
  "files": [
32
32
  "dist"
33
33
  ],
34
+ "dependencies": {
35
+ "@floegence/redevplugin-contracts": "0.6.5"
36
+ },
34
37
  "publishConfig": {
35
38
  "access": "public"
36
39
  },