@dimina-kit/devtools 0.3.2-dev.20260522110734 → 0.3.2-dev.20260522142649
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/README.md +90 -155
- package/dist/main/api.d.ts +3 -11
- package/dist/main/api.js +2 -13
- package/dist/main/app/app.d.ts +14 -1
- package/dist/main/app/app.js +93 -2
- package/dist/main/index.bundle.js +140 -52
- package/dist/main/ipc/app.d.ts +1 -1
- package/dist/main/ipc/app.js +3 -0
- package/dist/main/ipc/simulator.d.ts +1 -1
- package/dist/main/ipc/simulator.js +2 -3
- package/dist/main/ipc/toolbar.d.ts +1 -1
- package/dist/main/ipc/toolbar.js +14 -4
- package/dist/main/services/layout/index.d.ts +4 -10
- package/dist/main/services/layout/index.js +11 -19
- package/dist/main/services/module.d.ts +1 -2
- package/dist/main/services/simulator/custom-apis.d.ts +0 -1
- package/dist/main/services/simulator/custom-apis.js +0 -1
- package/dist/main/services/toolbar/toolbar-store.d.ts +23 -0
- package/dist/main/services/toolbar/toolbar-store.js +23 -0
- package/dist/main/services/views/view-manager.d.ts +6 -0
- package/dist/main/services/views/view-manager.js +6 -3
- package/dist/main/services/workbench-context.d.ts +40 -14
- package/dist/main/services/workbench-context.js +6 -1
- package/dist/main/utils/disposable.d.ts +5 -0
- package/dist/main/utils/disposable.js +7 -0
- package/dist/main/utils/ipc-registry.js +5 -1
- package/dist/main/utils/sender-policy.d.ts +4 -1
- package/dist/main/utils/sender-policy.js +6 -0
- package/dist/preload/runtime/custom-apis.js +72 -1
- package/dist/preload/windows/simulator.js +41 -1
- package/dist/renderer/assets/index-4lkyF644.js +46 -0
- package/dist/renderer/assets/{input-CoWcfa8u.js → input-D4byFOLS.js} +2 -2
- package/dist/renderer/assets/{ipc-transport-Cd2xPrsa.js → ipc-transport-BzPzxPwf.js} +2 -2
- package/dist/renderer/assets/{popover-Nq5gT9DO.js → popover-cxKrg_os.js} +2 -2
- package/dist/renderer/assets/{select-By5KH-UG.js → select-BgSkkdHW.js} +2 -2
- package/dist/renderer/assets/{settings-api-Do0_Smq1.js → settings-api-BXCw9vOZ.js} +2 -2
- package/dist/renderer/assets/{settings-Dj_N70qR.js → settings-oD2K1jdT.js} +2 -2
- package/dist/renderer/assets/{workbenchSettings-CazSLGYt.js → workbenchSettings-CTNBpTft.js} +2 -2
- package/dist/renderer/entries/main/index.html +4 -4
- package/dist/renderer/entries/popover/index.html +4 -4
- package/dist/renderer/entries/settings/index.html +4 -4
- package/dist/renderer/entries/workbench-settings/index.html +3 -3
- package/dist/shared/ipc-channels.d.ts +3 -2
- package/dist/shared/ipc-channels.js +4 -3
- package/dist/shared/ipc-schemas.d.ts +2 -0
- package/dist/shared/ipc-schemas.js +2 -0
- package/dist/shared/types.d.ts +55 -3
- package/dist/simulator/assets/simulator-CILbo0Ji.js +9 -0
- package/dist/simulator/simulator.html +1 -1
- package/package.json +3 -35
- package/dist/main/simulator-apis.d.ts +0 -23
- package/dist/main/simulator-apis.js +0 -24
- package/dist/renderer/assets/index-D8VFYBh-.js +0 -46
- package/dist/simulator/assets/simulator-CZc88Jeh.js +0 -9
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import type { BrowserWindow } from 'electron';
|
|
2
|
-
import type { CompilationAdapter } from '../../shared/types.js';
|
|
2
|
+
import type { CompilationAdapter, WorkbenchConfig } from '../../shared/types.js';
|
|
3
3
|
import { DisposableRegistry } from '../utils/disposable.js';
|
|
4
4
|
import type { SenderPolicy } from '../utils/ipc-registry.js';
|
|
5
5
|
import { type RendererNotifier } from './notifications/renderer-notifier.js';
|
|
6
6
|
import { type ViewManager } from './views/view-manager.js';
|
|
7
7
|
import { type WindowService } from './window-service.js';
|
|
8
8
|
import { type WorkspaceService } from './workspace/workspace-service.js';
|
|
9
|
+
import { type SimulatorApiRegistry } from './simulator/custom-apis.js';
|
|
10
|
+
import { type ToolbarStore } from './toolbar/toolbar-store.js';
|
|
9
11
|
import { sanitizeTemplates } from './projects/templates.js';
|
|
10
12
|
import type { ProjectsProvider, ProjectTemplate } from './projects/types.js';
|
|
11
13
|
import type { CustomCreateProjectDialogResult } from '../../shared/types.js';
|
|
@@ -25,14 +27,8 @@ export interface WorkbenchContext {
|
|
|
25
27
|
apiNamespaces: string[];
|
|
26
28
|
/** Branding name shown in title bar and getBranding IPC */
|
|
27
29
|
appName: string;
|
|
28
|
-
/**
|
|
29
|
-
|
|
30
|
-
id: string;
|
|
31
|
-
label: string;
|
|
32
|
-
}>> | Array<{
|
|
33
|
-
id: string;
|
|
34
|
-
label: string;
|
|
35
|
-
}>;
|
|
30
|
+
/** Header bar height in px, used for view layout and exposed to the renderer */
|
|
31
|
+
headerHeight: number;
|
|
36
32
|
/** Host-injected provider for branding info (overrides default appName) */
|
|
37
33
|
brandingProvider?: () => Promise<{
|
|
38
34
|
appName: string;
|
|
@@ -77,18 +73,48 @@ export interface WorkbenchContext {
|
|
|
77
73
|
* lazily so it stays correct as windows/views come and go.
|
|
78
74
|
*/
|
|
79
75
|
senderPolicy: SenderPolicy;
|
|
76
|
+
/**
|
|
77
|
+
* Reference-counted map of `webContents.id` → live registration count for
|
|
78
|
+
* host-owned BrowserWindows registered as trusted senders via
|
|
79
|
+
* `instance.registerTrustedWindow`. A window stays trusted while its count
|
|
80
|
+
* is > 0; registering the same window N times requires N disposals (or a
|
|
81
|
+
* single `closed` event, which zeroes the count outright) to un-trust it.
|
|
82
|
+
* Consulted by `createWorkbenchSenderPolicy` in addition to the static
|
|
83
|
+
* main-window / overlay checks.
|
|
84
|
+
*/
|
|
85
|
+
trustedWindowSenderIds: Map<number, number>;
|
|
86
|
+
/**
|
|
87
|
+
* Per-context registry of host-registered simulator custom APIs. Populated
|
|
88
|
+
* via `instance.registerSimulatorApi`; read by the simulator IPC handlers.
|
|
89
|
+
* One registry per context — no process-global crosstalk.
|
|
90
|
+
*/
|
|
91
|
+
simulatorApis: SimulatorApiRegistry;
|
|
92
|
+
/**
|
|
93
|
+
* Per-context store of host-registered toolbar actions. Populated via
|
|
94
|
+
* `instance.toolbar.set`; read by the toolbar IPC handlers. One store per
|
|
95
|
+
* context — no process-global crosstalk.
|
|
96
|
+
*/
|
|
97
|
+
toolbar: ToolbarStore;
|
|
80
98
|
/** Aggregates dispose handlers for every IPC handler, listener, watcher, and CDP session registered by the workbench. */
|
|
81
99
|
registry: DisposableRegistry;
|
|
82
100
|
}
|
|
83
|
-
|
|
101
|
+
/**
|
|
102
|
+
* Inputs for `createWorkbenchContext`. The scalar config fields it shares with
|
|
103
|
+
* `WorkbenchConfig` (`adapter` / `apiNamespaces` / `appName` / `headerHeight`)
|
|
104
|
+
* are derived from there via `Pick` so the two stay in lockstep — no
|
|
105
|
+
* field-by-field re-declaration. The remaining fields are kept explicit
|
|
106
|
+
* because their shapes intentionally differ from the config:
|
|
107
|
+
* - `preloadPath` / `rendererDir` are REQUIRED here (the caller resolves the
|
|
108
|
+
* defaults before constructing the context) but optional in the config;
|
|
109
|
+
* - `panels` is the structural `string[]` (the context stores raw ids);
|
|
110
|
+
* - `projectsProvider` / `projectTemplates` / `customCreateProjectDialog` use
|
|
111
|
+
* the main-process types, not the structural mirrors in `shared/types`.
|
|
112
|
+
*/
|
|
113
|
+
export interface CreateContextOptions extends Pick<WorkbenchConfig, 'adapter' | 'apiNamespaces' | 'appName' | 'headerHeight'> {
|
|
84
114
|
mainWindow: BrowserWindow;
|
|
85
|
-
adapter?: CompilationAdapter;
|
|
86
115
|
preloadPath: string;
|
|
87
116
|
rendererDir: string;
|
|
88
117
|
panels?: string[];
|
|
89
|
-
apiNamespaces?: string[];
|
|
90
|
-
appName?: string;
|
|
91
|
-
toolbarActions?: WorkbenchContext['toolbarActions'];
|
|
92
118
|
brandingProvider?: WorkbenchContext['brandingProvider'];
|
|
93
119
|
/** Host-supplied project list backend. Defaults to LocalProjectsProvider. */
|
|
94
120
|
projectsProvider?: ProjectsProvider;
|
|
@@ -6,6 +6,8 @@ import { createViewManager } from './views/view-manager.js';
|
|
|
6
6
|
import { createWindowService } from './window-service.js';
|
|
7
7
|
import { createWorkspaceService, } from './workspace/workspace-service.js';
|
|
8
8
|
import { createLocalProjectsProvider } from './projects/local-provider.js';
|
|
9
|
+
import { createSimulatorApiRegistry, } from './simulator/custom-apis.js';
|
|
10
|
+
import { createToolbarStore } from './toolbar/toolbar-store.js';
|
|
9
11
|
import { resolveTemplates, sanitizeTemplates } from './projects/templates.js';
|
|
10
12
|
import { BUILTIN_TEMPLATES } from './projects/builtin-templates.js';
|
|
11
13
|
export function hasBuiltinPanel(ctx, panelId) {
|
|
@@ -26,10 +28,13 @@ export function createWorkbenchContext(opts) {
|
|
|
26
28
|
panels: opts.panels ?? ['wxml', 'console', 'appdata', 'storage'],
|
|
27
29
|
apiNamespaces: opts.apiNamespaces ?? [],
|
|
28
30
|
appName: opts.appName ?? 'Dimina DevTools',
|
|
29
|
-
|
|
31
|
+
headerHeight: opts.headerHeight ?? 40,
|
|
30
32
|
brandingProvider: opts.brandingProvider,
|
|
31
33
|
};
|
|
32
34
|
ctx.registry = new DisposableRegistry();
|
|
35
|
+
ctx.trustedWindowSenderIds = new Map();
|
|
36
|
+
ctx.simulatorApis = createSimulatorApiRegistry();
|
|
37
|
+
ctx.toolbar = createToolbarStore();
|
|
33
38
|
ctx.windows = createWindowService(opts.mainWindow);
|
|
34
39
|
ctx.views = createViewManager(ctx);
|
|
35
40
|
ctx.notify = createRendererNotifier(ctx);
|
|
@@ -6,6 +6,11 @@ export declare function toDisposable(fn: DisposeFn): Disposable;
|
|
|
6
6
|
export declare class DisposableRegistry implements Disposable {
|
|
7
7
|
private entries;
|
|
8
8
|
private _disposed;
|
|
9
|
+
/**
|
|
10
|
+
* Number of live entries. A wrapper's `dispose` splices its entry out and
|
|
11
|
+
* `disposeAll` clears the array, so `entries.length` is the live count.
|
|
12
|
+
*/
|
|
13
|
+
get size(): number;
|
|
9
14
|
add(d: Disposable | DisposeFn): Disposable;
|
|
10
15
|
disposeAll(): Promise<void>;
|
|
11
16
|
dispose(): Promise<void>;
|
|
@@ -12,6 +12,13 @@ export function toDisposable(fn) {
|
|
|
12
12
|
export class DisposableRegistry {
|
|
13
13
|
entries = [];
|
|
14
14
|
_disposed = false;
|
|
15
|
+
/**
|
|
16
|
+
* Number of live entries. A wrapper's `dispose` splices its entry out and
|
|
17
|
+
* `disposeAll` clears the array, so `entries.length` is the live count.
|
|
18
|
+
*/
|
|
19
|
+
get size() {
|
|
20
|
+
return this.entries.length;
|
|
21
|
+
}
|
|
15
22
|
add(d) {
|
|
16
23
|
if (this._disposed) {
|
|
17
24
|
throw new Error('cannot add to disposed registry');
|
|
@@ -55,8 +55,12 @@ export class IpcRegistry {
|
|
|
55
55
|
}
|
|
56
56
|
handle(channel, fn) {
|
|
57
57
|
const policy = this.policy;
|
|
58
|
+
// The gate is `async` so a rejected sender surfaces as a *rejected
|
|
59
|
+
// promise* (the invoke-result contract) rather than a synchronous throw —
|
|
60
|
+
// synchronous throws can escape callers that wrap the result in
|
|
61
|
+
// `Promise.resolve(...)` instead of `await`-ing it directly.
|
|
58
62
|
const guarded = policy
|
|
59
|
-
? (event, ...args) => {
|
|
63
|
+
? async (event, ...args) => {
|
|
60
64
|
const sender = event.sender;
|
|
61
65
|
if (!policy(sender)) {
|
|
62
66
|
console.warn(`[ipc] sender rejected for channel '${channel}' (${summarizeSender(sender)})`);
|
|
@@ -18,8 +18,11 @@ import type { SenderPolicy } from './ipc-registry.js';
|
|
|
18
18
|
* Keeping the guest off this list contains the blast radius if the
|
|
19
19
|
* simulator content is ever compromised.
|
|
20
20
|
*
|
|
21
|
+
* Host-owned BrowserWindows registered via `instance.registerTrustedWindow`
|
|
22
|
+
* are additionally accepted via `ctx.trustedWindowSenderIds`.
|
|
23
|
+
*
|
|
21
24
|
* Any other sender — including a stale/destroyed sender or an unknown
|
|
22
25
|
* iframe — is rejected.
|
|
23
26
|
*/
|
|
24
|
-
export declare function createWorkbenchSenderPolicy(ctx: Pick<WorkbenchContext, 'windows' | 'views'>): SenderPolicy;
|
|
27
|
+
export declare function createWorkbenchSenderPolicy(ctx: Pick<WorkbenchContext, 'windows' | 'views' | 'trustedWindowSenderIds'>): SenderPolicy;
|
|
25
28
|
//# sourceMappingURL=sender-policy.d.ts.map
|
|
@@ -16,6 +16,9 @@
|
|
|
16
16
|
* Keeping the guest off this list contains the blast radius if the
|
|
17
17
|
* simulator content is ever compromised.
|
|
18
18
|
*
|
|
19
|
+
* Host-owned BrowserWindows registered via `instance.registerTrustedWindow`
|
|
20
|
+
* are additionally accepted via `ctx.trustedWindowSenderIds`.
|
|
21
|
+
*
|
|
19
22
|
* Any other sender — including a stale/destroyed sender or an unknown
|
|
20
23
|
* iframe — is rejected.
|
|
21
24
|
*/
|
|
@@ -29,6 +32,9 @@ export function createWorkbenchSenderPolicy(ctx) {
|
|
|
29
32
|
// Standalone settings BrowserWindow renderer
|
|
30
33
|
if (ctx.windows.isSettingsWindowSender(sender.id))
|
|
31
34
|
return true;
|
|
35
|
+
// Host-registered trusted windows (registerTrustedWindow)
|
|
36
|
+
if (ctx.trustedWindowSenderIds.has(sender.id))
|
|
37
|
+
return true;
|
|
32
38
|
// Settings overlay view (mounted inside the main window)
|
|
33
39
|
const settingsViewId = ctx.views.getSettingsWebContentsId();
|
|
34
40
|
if (settingsViewId != null && sender.id === settingsViewId)
|
|
@@ -1,6 +1,23 @@
|
|
|
1
1
|
import { ipcRenderer } from 'electron';
|
|
2
2
|
import { SimulatorCustomApiBridgeChannel } from '../../shared/ipc-channels.js';
|
|
3
3
|
import { exposeOnMainWorld } from '../shared/expose.js';
|
|
4
|
+
/**
|
|
5
|
+
* Re-send interval and ceiling for `list()` (see `listWithRetry`).
|
|
6
|
+
*
|
|
7
|
+
* The simulator boots fast enough that `__diminaCustomApis.list()` can fire
|
|
8
|
+
* its `sendToHost` request *before* the embedding main-window renderer has
|
|
9
|
+
* attached its `ipc-message` proxy listener (`useCustomApiProxy`). `sendToHost`
|
|
10
|
+
* is fire-and-forget — a request sent into that gap is lost, and the bridge
|
|
11
|
+
* call would otherwise hang until `custom-api-boot.ts`'s 3s deadlock-breaker.
|
|
12
|
+
*
|
|
13
|
+
* `list` is an idempotent read, so it is safe to re-send until a response
|
|
14
|
+
* arrives. The retry resolves within one embedder attach cycle (~200ms) in
|
|
15
|
+
* practice; the ceiling only bounds the leak if the proxy never attaches at
|
|
16
|
+
* all, and is kept below `custom-api-boot.ts`'s `CUSTOM_API_LIST_TIMEOUT_MS`
|
|
17
|
+
* so the retry exhausts before — not after — that outer timeout would fire.
|
|
18
|
+
*/
|
|
19
|
+
const LIST_RETRY_INTERVAL_MS = 150;
|
|
20
|
+
const LIST_RETRY_CEILING_MS = 2500;
|
|
4
21
|
// The simulator <webview> is intentionally kept off the workbench sender-policy
|
|
5
22
|
// white-list, so it cannot reach `ipcMain.handle` directly. Instead the bridge
|
|
6
23
|
// asks the trusted main-window renderer to proxy the call: webview sends via
|
|
@@ -31,8 +48,62 @@ function buildBridge() {
|
|
|
31
48
|
ipcRenderer.sendToHost(SimulatorCustomApiBridgeChannel.Request, req);
|
|
32
49
|
});
|
|
33
50
|
};
|
|
51
|
+
/**
|
|
52
|
+
* `list()` with re-send, to survive the embedder proxy attaching after the
|
|
53
|
+
* first request (see `LIST_RETRY_INTERVAL_MS`). Each attempt uses a fresh id
|
|
54
|
+
* and its own `pending` entry; whichever response lands first settles the
|
|
55
|
+
* call. Stale entries for the other attempts are dropped so a late duplicate
|
|
56
|
+
* response cannot resolve nothing — or, worse, leak.
|
|
57
|
+
*/
|
|
58
|
+
const listWithRetry = () => {
|
|
59
|
+
return new Promise((resolve, reject) => {
|
|
60
|
+
const attemptIds = new Set();
|
|
61
|
+
// Boxed so `cleanup` (defined before the timers are created) can clear
|
|
62
|
+
// them; the box itself is `const`, only its fields are assigned later.
|
|
63
|
+
const state = { settled: false };
|
|
64
|
+
const cleanup = () => {
|
|
65
|
+
state.settled = true;
|
|
66
|
+
if (state.retryTimer)
|
|
67
|
+
clearInterval(state.retryTimer);
|
|
68
|
+
if (state.ceilingTimer)
|
|
69
|
+
clearTimeout(state.ceilingTimer);
|
|
70
|
+
for (const id of attemptIds)
|
|
71
|
+
pending.delete(id);
|
|
72
|
+
attemptIds.clear();
|
|
73
|
+
};
|
|
74
|
+
const attempt = () => {
|
|
75
|
+
if (state.settled)
|
|
76
|
+
return;
|
|
77
|
+
const id = nextId++;
|
|
78
|
+
attemptIds.add(id);
|
|
79
|
+
pending.set(id, {
|
|
80
|
+
resolve: (value) => {
|
|
81
|
+
if (state.settled)
|
|
82
|
+
return;
|
|
83
|
+
cleanup();
|
|
84
|
+
resolve(value);
|
|
85
|
+
},
|
|
86
|
+
reject: (reason) => {
|
|
87
|
+
if (state.settled)
|
|
88
|
+
return;
|
|
89
|
+
cleanup();
|
|
90
|
+
reject(reason);
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
ipcRenderer.sendToHost(SimulatorCustomApiBridgeChannel.Request, { id, op: 'list' });
|
|
94
|
+
};
|
|
95
|
+
state.retryTimer = setInterval(attempt, LIST_RETRY_INTERVAL_MS);
|
|
96
|
+
state.ceilingTimer = setTimeout(() => {
|
|
97
|
+
if (state.settled)
|
|
98
|
+
return;
|
|
99
|
+
cleanup();
|
|
100
|
+
reject(new Error('custom-apis bridge list() got no response from the host renderer'));
|
|
101
|
+
}, LIST_RETRY_CEILING_MS);
|
|
102
|
+
attempt();
|
|
103
|
+
});
|
|
104
|
+
};
|
|
34
105
|
return {
|
|
35
|
-
list:
|
|
106
|
+
list: listWithRetry,
|
|
36
107
|
invoke: (name, params) => send({ id: nextId++, op: 'invoke', name, params }),
|
|
37
108
|
};
|
|
38
109
|
}
|
|
@@ -198,6 +198,8 @@ var MiniappSnapshotChannel = {
|
|
|
198
198
|
};
|
|
199
199
|
|
|
200
200
|
// src/preload/runtime/custom-apis.ts
|
|
201
|
+
var LIST_RETRY_INTERVAL_MS = 150;
|
|
202
|
+
var LIST_RETRY_CEILING_MS = 2500;
|
|
201
203
|
function buildBridge() {
|
|
202
204
|
let nextId = 1;
|
|
203
205
|
const pending = /* @__PURE__ */ new Map();
|
|
@@ -220,8 +222,46 @@ function buildBridge() {
|
|
|
220
222
|
import_electron2.ipcRenderer.sendToHost(SimulatorCustomApiBridgeChannel.Request, req);
|
|
221
223
|
});
|
|
222
224
|
};
|
|
225
|
+
const listWithRetry = () => {
|
|
226
|
+
return new Promise((resolve, reject) => {
|
|
227
|
+
const attemptIds = /* @__PURE__ */ new Set();
|
|
228
|
+
const state2 = { settled: false };
|
|
229
|
+
const cleanup = () => {
|
|
230
|
+
state2.settled = true;
|
|
231
|
+
if (state2.retryTimer) clearInterval(state2.retryTimer);
|
|
232
|
+
if (state2.ceilingTimer) clearTimeout(state2.ceilingTimer);
|
|
233
|
+
for (const id of attemptIds) pending.delete(id);
|
|
234
|
+
attemptIds.clear();
|
|
235
|
+
};
|
|
236
|
+
const attempt = () => {
|
|
237
|
+
if (state2.settled) return;
|
|
238
|
+
const id = nextId++;
|
|
239
|
+
attemptIds.add(id);
|
|
240
|
+
pending.set(id, {
|
|
241
|
+
resolve: (value) => {
|
|
242
|
+
if (state2.settled) return;
|
|
243
|
+
cleanup();
|
|
244
|
+
resolve(value);
|
|
245
|
+
},
|
|
246
|
+
reject: (reason) => {
|
|
247
|
+
if (state2.settled) return;
|
|
248
|
+
cleanup();
|
|
249
|
+
reject(reason);
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
import_electron2.ipcRenderer.sendToHost(SimulatorCustomApiBridgeChannel.Request, { id, op: "list" });
|
|
253
|
+
};
|
|
254
|
+
state2.retryTimer = setInterval(attempt, LIST_RETRY_INTERVAL_MS);
|
|
255
|
+
state2.ceilingTimer = setTimeout(() => {
|
|
256
|
+
if (state2.settled) return;
|
|
257
|
+
cleanup();
|
|
258
|
+
reject(new Error("custom-apis bridge list() got no response from the host renderer"));
|
|
259
|
+
}, LIST_RETRY_CEILING_MS);
|
|
260
|
+
attempt();
|
|
261
|
+
});
|
|
262
|
+
};
|
|
223
263
|
return {
|
|
224
|
-
list:
|
|
264
|
+
list: listWithRetry,
|
|
225
265
|
invoke: (name, params) => send({ id: nextId++, op: "invoke", name, params })
|
|
226
266
|
};
|
|
227
267
|
}
|