@unbound-app/api 0.11.0 → 1.1.1

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.
Files changed (72) hide show
  1. package/ambient/image-modules.d.ts +58 -0
  2. package/package.json +27 -4
  3. package/src/_internal.d.ts +2775 -139
  4. package/src/{utils.d.ts → _types.d.ts} +10 -0
  5. package/src/assets.d.ts +64 -26
  6. package/src/builtins.d.ts +17 -0
  7. package/src/commands.d.ts +101 -25
  8. package/src/dialogs.d.ts +90 -25
  9. package/src/flux.d.ts +317 -0
  10. package/src/fonts.d.ts +51 -0
  11. package/src/fs.d.ts +15 -24
  12. package/src/global.d.ts +173 -11
  13. package/src/i18n.d.ts +33 -31
  14. package/src/iconPacks.d.ts +43 -0
  15. package/src/index.d.ts +14 -0
  16. package/src/marketplace.d.ts +50 -0
  17. package/src/metro/api.d.ts +8 -0
  18. package/src/metro/common.d.ts +34 -5
  19. package/src/metro/components.d.ts +34 -25
  20. package/src/metro/filters.d.ts +31 -16
  21. package/src/metro/index.d.ts +26079 -83
  22. package/src/metro/stores.d.ts +5 -0
  23. package/src/native.d.ts +77 -5
  24. package/src/patcher.d.ts +214 -57
  25. package/src/plugins.d.ts +55 -0
  26. package/src/settings.d.ts +8 -16
  27. package/src/shims.d.ts +5 -0
  28. package/src/storage.d.ts +80 -224
  29. package/src/sync.d.ts +51 -0
  30. package/src/themes.d.ts +43 -0
  31. package/src/toasts.d.ts +62 -9
  32. package/src/ui.d.ts +650 -0
  33. package/src/updates.d.ts +25 -0
  34. package/src/utils/index.d.ts +53 -0
  35. package/src/utils/react/index.d.ts +82 -0
  36. package/testing/fake-api.ts +256 -0
  37. package/testing/fake-components.ts +183 -0
  38. package/testing/fake-feedback.ts +258 -0
  39. package/testing/fake-fs.ts +54 -0
  40. package/testing/fake-java.ts +513 -0
  41. package/testing/fake-locales.ts +85 -0
  42. package/testing/fake-metro.ts +346 -0
  43. package/testing/fake-native-bridge.ts +98 -0
  44. package/testing/fake-native.ts +211 -0
  45. package/testing/fake-objc.ts +388 -0
  46. package/testing/fake-plugins.ts +176 -0
  47. package/testing/fake-react-native-modules.ts +362 -0
  48. package/testing/fake-react-native.ts +180 -0
  49. package/testing/fake-react.ts +54 -0
  50. package/testing/fake-settings-panel.ts +173 -0
  51. package/testing/fake-settings.ts +428 -0
  52. package/testing/fake-timers.ts +74 -0
  53. package/testing/fake-utils.ts +216 -0
  54. package/testing/icu-format.ts +132 -0
  55. package/testing/icu-parse.ts +277 -0
  56. package/testing/index.ts +42 -0
  57. package/testing/install-addon-globals.ts +260 -0
  58. package/testing/listener-registry.ts +39 -0
  59. package/testing/memory-storage.ts +149 -0
  60. package/testing/plugin-files.ts +155 -0
  61. package/testing/plugin-lifecycle.ts +149 -0
  62. package/testing/plugin-types.ts +34 -0
  63. package/testing/preset.ts +263 -0
  64. package/testing/react-patcher.ts +154 -0
  65. package/testing/react-value.ts +3 -0
  66. package/testing/recorders.ts +183 -0
  67. package/testing/recording-patcher.ts +173 -0
  68. package/testing/render.ts +84 -0
  69. package/testing/session.ts +77 -0
  70. package/testing/test-node.ts +163 -0
  71. package/testing/test-renderer.ts +753 -0
  72. package/testing/unfrozen-elements.ts +49 -0
@@ -0,0 +1,388 @@
1
+ import type {
2
+ NativeAssociationKey,
3
+ NativeBatchReference,
4
+ NativeClassHandle,
5
+ NativeHandle,
6
+ NativeHookContext,
7
+ NativeHookHandlers,
8
+ NativeHookOptions,
9
+ NativeHookToken,
10
+ NativeObjCBridge,
11
+ NativeObjCCallPlan,
12
+ NativeObjectHandle,
13
+ NativeStruct,
14
+ } from '../src/native';
15
+
16
+ import {
17
+ brand,
18
+ createCallLog,
19
+ settled,
20
+ withMissingMembers,
21
+ type NativeCall,
22
+ type NativeCallLog,
23
+ } from './fake-native-bridge';
24
+
25
+ /** What an Objective-C call returns: a plain value, a handle, a 64-bit integer, or nothing. */
26
+ type ObjcResult = ReturnType<NativeObjCBridge['call']>;
27
+
28
+ /** A method a test registers on a class: it receives the object (or the class, for a class method) and the call's arguments. */
29
+ // UNKNOWN: a registered method takes whatever arguments the plugin under test passes.
30
+ export type ObjcMethod = (self: NativeObjectHandle, ...args: unknown[]) => ObjcResult;
31
+
32
+ /** What a test registers for an Objective-C class. */
33
+ export type ObjcClassSpec = {
34
+ /** The class whose methods this one inherits, and `callSuper` reaches. */
35
+ superclass?: string;
36
+ /** Instance methods by selector, such as `setAlpha:`. */
37
+ methods?: Record<string, ObjcMethod>;
38
+ /** Class methods by selector, such as `sharedApplication`. */
39
+ classMethods?: Record<string, ObjcMethod>;
40
+ };
41
+
42
+ /** The fake Objective-C bridge a plugin calls, and what a test uses to fill it. */
43
+ export type FakeObjc = {
44
+ /** The `objc` a plugin's `context.native` holds. */
45
+ bridge: NativeObjCBridge;
46
+ /** Adds a class a plugin can find with `objc.getClass`, or replaces it. */
47
+ registerClass: (name: string, spec?: ObjcClassSpec) => void;
48
+ /** Makes an object of a registered class, with instance variables a plugin reads through `getIvar`. */
49
+ create: (className: string, ivars?: Record<string, ObjcResult>) => NativeObjectHandle;
50
+ /** Forgets every class, object and hook. */
51
+ reset: () => void;
52
+ };
53
+
54
+ type Hook = {
55
+ className: string;
56
+ selector: string;
57
+ handlers: NativeHookHandlers;
58
+ options: NativeHookOptions;
59
+ token: NativeHookToken & { active: boolean };
60
+ };
61
+
62
+ // A handle is an opaque brand on device; here it is a real object the fake reads its class and variables from.
63
+ class ObjcHandle {
64
+ readonly className: string;
65
+ readonly isClass: boolean;
66
+ readonly ivars = new Map<string, ObjcResult>();
67
+ readonly associated = new Map<NativeAssociationKey, ObjcResult>();
68
+ selector = '';
69
+
70
+ constructor(className: string, isClass: boolean) {
71
+ this.className = className;
72
+ this.isClass = isClass;
73
+ }
74
+ }
75
+
76
+ // A struct is an opaque brand on device; the fake keeps its name and fields.
77
+ class ObjcStruct<T extends object> {
78
+ readonly name: string;
79
+ readonly value: T;
80
+
81
+ constructor(name: string, value: T) {
82
+ this.name = name;
83
+ this.value = value;
84
+ }
85
+ }
86
+
87
+ // UNKNOWN: any value may be passed where a handle is expected, and only an object the fake made is one.
88
+ function handleOf(value: unknown): ObjcHandle | undefined {
89
+ return value instanceof ObjcHandle ? value : undefined;
90
+ }
91
+
92
+ // UNKNOWN: a batch step's target or argument is any value, or a reference to an earlier step's result.
93
+ function isBatchReference(value: unknown): value is NativeBatchReference {
94
+ return typeof value === 'object' && value !== null && '$nativeBatchResult' in value;
95
+ }
96
+
97
+ /**
98
+ * @description Creates the fake `objc`: classes and methods a test registers answer `invoke`, `call`, `create` and the rest,
99
+ * `hook` runs its handlers around a registered method, and every call is recorded in the log.
100
+ * @param log Where each call is recorded.
101
+ * @returns The bridge and what fills it.
102
+ */
103
+ export function createFakeObjc(log: NativeCallLog = createCallLog()): FakeObjc {
104
+ const classes = new Map<string, ObjcClassSpec>();
105
+ const hooks = new Set<Hook>();
106
+
107
+ function chain(className: string): string[] {
108
+ const names: string[] = [];
109
+
110
+ for (
111
+ let name: string | undefined = className;
112
+ name !== undefined && !names.includes(name);
113
+ name = classes.get(name)?.superclass
114
+ ) {
115
+ names.push(name);
116
+ }
117
+
118
+ return names;
119
+ }
120
+
121
+ function methodOf(className: string, selector: string, isClass: boolean, after?: string) {
122
+ const names = chain(className);
123
+
124
+ for (const name of names.slice(after === undefined ? 0 : names.indexOf(after) + 1)) {
125
+ const spec = classes.get(name);
126
+ const found = isClass ? spec?.classMethods?.[selector] : spec?.methods?.[selector];
127
+
128
+ if (found) {
129
+ return found;
130
+ }
131
+ }
132
+ }
133
+
134
+ function reference(handle: NativeHandle, call: string): ObjcHandle {
135
+ const found = handleOf(handle);
136
+
137
+ if (!found) {
138
+ throw new TypeError(
139
+ `Failed to call objc.${call}: the target is not an Objective-C handle.`,
140
+ );
141
+ }
142
+
143
+ return found;
144
+ }
145
+
146
+ function create(className: string, ivars: Record<string, ObjcResult> = {}): NativeObjectHandle {
147
+ const instance = new ObjcHandle(className, false);
148
+
149
+ for (const [name, value] of Object.entries(ivars)) instance.ivars.set(name, value);
150
+
151
+ return brand<NativeObjectHandle>(instance);
152
+ }
153
+
154
+ function className(handle: NativeHandle): string | null {
155
+ return handleOf(handle)?.className ?? null;
156
+ }
157
+
158
+ function classNameOf(name: string | NativeClassHandle): string {
159
+ return typeof name === 'string' ? name : (className(name) ?? 'unknown');
160
+ }
161
+
162
+ // The calls every class answers: `alloc` and `new` make an object, an `init…` returns the receiver.
163
+ function builtIn(target: ObjcHandle, selector: string): ObjcMethod | undefined {
164
+ if (target.isClass && (selector === 'alloc' || selector === 'new')) {
165
+ return () => create(target.className);
166
+ }
167
+
168
+ if (!target.isClass && selector.startsWith('init')) {
169
+ return (self) => self;
170
+ }
171
+ }
172
+
173
+ // UNKNOWN: the arguments and result are whatever the plugin and the test's fake method agree on.
174
+ function runHooks(
175
+ target: ObjcHandle,
176
+ selector: string,
177
+ args: NativeCall['args'],
178
+ original: NativeHookContext['original'],
179
+ ): ObjcResult {
180
+ const self = brand<NativeObjectHandle>(target);
181
+ const active = [...hooks].filter(
182
+ (hook) =>
183
+ hook.selector === selector &&
184
+ chain(target.className).includes(hook.className) &&
185
+ (hook.options.instance === undefined || hook.options.instance === self),
186
+ );
187
+
188
+ function context(phase: NativeHookContext['phase']): NativeHookContext {
189
+ return {
190
+ self,
191
+ selector,
192
+ args,
193
+ argumentIndexes: args.map((_, index) => index),
194
+ phase,
195
+ original,
196
+ replaceObjectArgument: (index, value) => void (args[index] = value),
197
+ };
198
+ }
199
+
200
+ for (const hook of active) hook.handlers.before?.(context('before'));
201
+
202
+ const replacing = active.find((hook) => hook.handlers.replace);
203
+ const result = replacing?.handlers.replace?.(context('replace')) ?? original();
204
+
205
+ for (const hook of active) hook.handlers.after?.(context('after'));
206
+
207
+ for (const hook of active) {
208
+ if (hook.options.once) {
209
+ hook.token.remove();
210
+ }
211
+ }
212
+
213
+ return result;
214
+ }
215
+
216
+ // UNKNOWN: the arguments and result are whatever the plugin and the test's fake method agree on.
217
+ function send(
218
+ handle: NativeHandle,
219
+ selector: string,
220
+ args: NativeCall['args'],
221
+ call: string,
222
+ after?: string,
223
+ ): ObjcResult {
224
+ const target = reference(handle, call);
225
+
226
+ log.record({
227
+ bridge: 'objc',
228
+ method: call,
229
+ target: target.className,
230
+ name: selector,
231
+ args,
232
+ });
233
+
234
+ const method =
235
+ methodOf(target.className, selector, target.isClass, after) ??
236
+ builtIn(target, selector);
237
+
238
+ if (!method) {
239
+ throw new Error(
240
+ `Failed to call ${target.className} ${selector}: the test registered no such method. Add it with native.objc.registerClass('${target.className}', { ${target.isClass ? 'classMethods' : 'methods'}: { '${selector}': () => … } }).`,
241
+ );
242
+ }
243
+
244
+ const self = brand<NativeObjectHandle>(target);
245
+
246
+ return runHooks(target, selector, args, (...next) =>
247
+ method(self, ...(next.length ? next : args)),
248
+ );
249
+ }
250
+
251
+ // UNKNOWN: the arguments and result are whatever the plugin and the test's fake method agree on.
252
+ function invokePrepared(plan: NativeHandle, handle: NativeHandle, args: NativeCall['args']) {
253
+ return send(handle, reference(plan, 'invokePrepared').selector, args, 'invokePrepared');
254
+ }
255
+
256
+ function respondsTo(handle: NativeHandle, selector: string): boolean {
257
+ const target = handleOf(handle);
258
+
259
+ return Boolean(
260
+ target &&
261
+ (methodOf(target.className, selector, target.isClass) || builtIn(target, selector)),
262
+ );
263
+ }
264
+
265
+ // UNKNOWN: a batch step's target or argument is any value, or a reference to an earlier step's result.
266
+ function resolveStep(step: unknown, results: ObjcResult[]) {
267
+ return isBatchReference(step) ? results[step.$nativeBatchResult] : step;
268
+ }
269
+
270
+ const bridge: NativeObjCBridge = withMissingMembers<NativeObjCBridge>('objc', {
271
+ getClass(name) {
272
+ log.record({ bridge: 'objc', method: 'getClass', target: name, name: '', args: [] });
273
+
274
+ return classes.has(name) ? brand<NativeClassHandle>(new ObjcHandle(name, true)) : null;
275
+ },
276
+ className,
277
+ alloc(classOrName) {
278
+ const name = classNameOf(classOrName);
279
+
280
+ log.record({ bridge: 'objc', method: 'alloc', target: name, name: 'alloc', args: [] });
281
+
282
+ if (!classes.has(name)) {
283
+ throw new Error(`Objective-C class ${name} is not available.`);
284
+ }
285
+
286
+ return create(name);
287
+ },
288
+ create(classOrName, initSelector, args = []) {
289
+ const name = classNameOf(classOrName);
290
+
291
+ if (!classes.has(name)) {
292
+ throw new Error(`Objective-C class ${name} is not available.`);
293
+ }
294
+
295
+ const instance = create(name);
296
+ const initialized = send(instance, initSelector, args, 'create');
297
+
298
+ const returned = handleOf(initialized);
299
+
300
+ return returned ? brand<NativeObjectHandle>(returned) : instance;
301
+ },
302
+ respondsTo,
303
+ call: (handle, selector, ...args) => send(handle, selector, args, 'call'),
304
+ callSuper: (handle, currentClass, selector, ...args) =>
305
+ send(handle, selector, args, 'callSuper', classNameOf(currentClass)),
306
+ invoke: (handle, selector, args) => send(handle, selector, args, 'invoke'),
307
+ invokeAsync: (handle, selector, args) =>
308
+ settled(() => send(handle, selector, args, 'invokeAsync')),
309
+ invokeSuper: (handle, currentClass, selector, args) =>
310
+ send(handle, selector, args, 'invokeSuper', classNameOf(currentClass)),
311
+ invokeSuperAsync: (handle, currentClass, selector, args) =>
312
+ settled(() =>
313
+ send(handle, selector, args, 'invokeSuperAsync', classNameOf(currentClass)),
314
+ ),
315
+ prepare(handle, selector) {
316
+ const plan = new ObjcHandle('plan', false);
317
+
318
+ plan.selector = selector;
319
+
320
+ return brand<NativeObjCCallPlan>(plan);
321
+ },
322
+ invokePrepared,
323
+ invokePreparedAsync: (plan, handle, args) =>
324
+ settled(() => invokePrepared(plan, handle, args)),
325
+ disposePlan: () => {},
326
+ batch: (steps) =>
327
+ settled(() => {
328
+ const results: ObjcResult[] = [];
329
+
330
+ for (const step of steps) {
331
+ const target = resolveStep(step.target, results);
332
+ const args = step.args.map((arg) => resolveStep(arg, results));
333
+
334
+ results.push(send(brand(Object(target)), step.selector, args, 'batch'));
335
+ }
336
+
337
+ return results;
338
+ }),
339
+ getIvar: (handle, name) => reference(handle, 'getIvar').ivars.get(name),
340
+ setIvar: (handle, name, value) => void reference(handle, 'setIvar').ivars.set(name, value),
341
+ createAssociationKey: () => brand<NativeAssociationKey>(new ObjcHandle('key', false)),
342
+ getAssociatedObject: (handle, key) =>
343
+ reference(handle, 'getAssociatedObject').associated.get(key),
344
+ setAssociatedObject: (handle, key, value) =>
345
+ void reference(handle, 'setAssociatedObject').associated.set(key, value),
346
+ struct: (name, fields) => brand<NativeStruct<typeof fields>>(new ObjcStruct(name, fields)),
347
+ hook(hookClass, selector, handlers, options = {}) {
348
+ const token = {
349
+ active: true,
350
+ setReturnValue: () => {},
351
+ removeReturnValue: () => {},
352
+ clearReturnValues: () => {},
353
+ remove() {
354
+ token.active = false;
355
+ hooks.delete(hook);
356
+ },
357
+ };
358
+ const hook: Hook = {
359
+ className: hookClass,
360
+ selector,
361
+ handlers: handlers as NativeHookHandlers,
362
+ options,
363
+ token,
364
+ };
365
+
366
+ hooks.add(hook);
367
+ log.record({
368
+ bridge: 'objc',
369
+ method: 'hook',
370
+ target: hookClass,
371
+ name: selector,
372
+ args: [],
373
+ });
374
+
375
+ return token;
376
+ },
377
+ });
378
+
379
+ return {
380
+ bridge,
381
+ registerClass: (name, spec = {}) => void classes.set(name, spec),
382
+ create,
383
+ reset() {
384
+ classes.clear();
385
+ hooks.clear();
386
+ },
387
+ };
388
+ }
@@ -0,0 +1,176 @@
1
+ import type { AddonAuthor, AddonHandle, AddonManifest } from '../src/global';
2
+ import type { FakeModule } from './fake-metro';
3
+
4
+ /** What a test sets about an installed plugin; the rest reads as a started, enabled plugin. */
5
+ export type PluginSpec = {
6
+ name?: string;
7
+ version?: string;
8
+ authors?: AddonAuthor[];
9
+ /** Whether the user has it enabled. Defaults to true. */
10
+ enabled?: boolean;
11
+ /** Whether it is running. Defaults to whether it is enabled and has not failed. */
12
+ started?: boolean;
13
+ /** The error it failed to start with; sets `failed` and shows in `plugins.errors`. */
14
+ error?: Error;
15
+ };
16
+
17
+ /** The fake `plugins`, its installed plugins, and what sets them. */
18
+ export type FakePlugins = {
19
+ namespace: FakeModule;
20
+ /** Adds an installed plugin, or changes the one with this id. */
21
+ set: (id: string, spec?: PluginSpec) => void;
22
+ /** Removes an installed plugin. */
23
+ remove: (id: string) => void;
24
+ /** Puts back the plugin under test alone, started and enabled. */
25
+ reset: () => void;
26
+ };
27
+
28
+ type State = Required<Pick<PluginSpec, 'name' | 'version' | 'authors' | 'enabled'>> &
29
+ Pick<PluginSpec, 'started' | 'error'>;
30
+
31
+ /**
32
+ * @description Creates the fake `plugins`: the plugin under test is installed, started and enabled, and a test adds others.
33
+ * `enable`, `disable`, `toggle`, `start` and `stop` change the state the handles report; `install`, `reload` and `update` settle at once and change nothing.
34
+ * @param own The manifest of the plugin under test.
35
+ * @returns The fake.
36
+ */
37
+ export function createFakePlugins(own: AddonManifest): FakePlugins {
38
+ const states = new Map<string, State>();
39
+ const handles = new Map<string, AddonHandle>();
40
+ const errors = new Map<PropertyKey, Error>();
41
+ const entities = new Map<PropertyKey, AddonHandle>();
42
+ const startDurations = new Map<string, number>();
43
+
44
+ function stateOf(id: string): State | undefined {
45
+ return states.get(id);
46
+ }
47
+
48
+ function isRunning(state: State): boolean {
49
+ return state.started ?? (state.enabled && !state.error);
50
+ }
51
+
52
+ function handleOf(id: string, manifest?: AddonManifest): AddonHandle {
53
+ function state(): State {
54
+ const found = stateOf(id);
55
+
56
+ if (!found) {
57
+ throw new Error(`Failed to read plugin ${id}: it is no longer installed.`);
58
+ }
59
+
60
+ return found;
61
+ }
62
+ const built: AddonManifest = manifest ?? {
63
+ ...own,
64
+ id,
65
+ name: state().name,
66
+ version: state().version,
67
+ authors: state().authors,
68
+ };
69
+
70
+ return {
71
+ id,
72
+ get name() {
73
+ return state().name;
74
+ },
75
+ get version() {
76
+ return state().version;
77
+ },
78
+ get authors() {
79
+ return state().authors;
80
+ },
81
+ manifest: built,
82
+ get started() {
83
+ return isRunning(state());
84
+ },
85
+ get failed() {
86
+ return state().error !== undefined;
87
+ },
88
+ isEnabled: () => state().enabled,
89
+ isPlugin: () => true,
90
+ enable: () => enable(id),
91
+ disable: () => disable(id),
92
+ reload: async () => {},
93
+ toJSON: () => built,
94
+ };
95
+ }
96
+
97
+ function set(id: string, spec: PluginSpec = {}) {
98
+ const previous = stateOf(id);
99
+
100
+ states.set(id, {
101
+ name: spec.name ?? previous?.name ?? id,
102
+ version: spec.version ?? previous?.version ?? '1.0.0',
103
+ authors: spec.authors ?? previous?.authors ?? [],
104
+ enabled: spec.enabled ?? previous?.enabled ?? true,
105
+ started: 'started' in spec ? spec.started : previous?.started,
106
+ error: 'error' in spec ? spec.error : previous?.error,
107
+ });
108
+
109
+ if (!handles.has(id)) {
110
+ const handle = handleOf(id, id === own.id ? own : undefined);
111
+
112
+ handles.set(id, handle);
113
+ entities.set(id, handle);
114
+ }
115
+
116
+ const error = stateOf(id)?.error;
117
+
118
+ if (error) {
119
+ errors.set(id, error);
120
+ } else {
121
+ errors.delete(id);
122
+ }
123
+ }
124
+
125
+ function remove(id: string) {
126
+ states.delete(id);
127
+ handles.delete(id);
128
+ entities.delete(id);
129
+ errors.delete(id);
130
+ }
131
+
132
+ function update(id: string, change: Partial<State>) {
133
+ const state = stateOf(id);
134
+
135
+ if (state) {
136
+ states.set(id, { ...state, ...change });
137
+ }
138
+ }
139
+
140
+ function enable(id: string) {
141
+ update(id, { enabled: true, started: undefined });
142
+ }
143
+
144
+ function disable(id: string) {
145
+ update(id, { enabled: false, started: false });
146
+ }
147
+
148
+ function reset() {
149
+ for (const id of Array.from(states.keys())) remove(id);
150
+
151
+ startDurations.clear();
152
+ set(own.id, { name: own.name, version: own.version, authors: own.authors });
153
+ }
154
+
155
+ reset();
156
+
157
+ const namespace: FakeModule = {
158
+ entities,
159
+ errors,
160
+ startDurations,
161
+ get: (id: string) => handles.get(id),
162
+ getAll: () => [...handles.values()],
163
+ isEnabled: (id: string) => stateOf(id)?.enabled ?? false,
164
+ enable,
165
+ disable,
166
+ toggle: (id: string) => (stateOf(id)?.enabled ? disable(id) : enable(id)),
167
+ start: (id: string) => update(id, { started: true }),
168
+ stop: (id: string) => update(id, { started: false }),
169
+ uninstall: async (id: string) => remove(id),
170
+ install: async () => undefined,
171
+ reload: async () => ({ ok: true }),
172
+ update: async () => false,
173
+ };
174
+
175
+ return { namespace, set, remove, reset };
176
+ }