ctrl-fx 0.0.1 → 0.1.0

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 (145) hide show
  1. package/dist/clipboard.d.ts +7 -0
  2. package/dist/clipboard.d.ts.map +1 -0
  3. package/dist/clipboard.js +3 -0
  4. package/dist/db/effects.d.ts +78 -0
  5. package/dist/db/effects.d.ts.map +1 -0
  6. package/dist/db/effects.js +98 -0
  7. package/dist/db/errors.d.ts +21 -0
  8. package/dist/db/errors.d.ts.map +1 -0
  9. package/dist/db/errors.js +13 -0
  10. package/dist/db/index.d.ts +32 -0
  11. package/dist/db/index.d.ts.map +1 -0
  12. package/dist/db/index.js +19 -0
  13. package/dist/db/keypath.d.ts +40 -0
  14. package/dist/db/keypath.d.ts.map +1 -0
  15. package/dist/db/keypath.js +20 -0
  16. package/dist/db/setup.d.ts +83 -0
  17. package/dist/db/setup.d.ts.map +1 -0
  18. package/dist/db/setup.js +100 -0
  19. package/dist/db/testdb.d.ts +19 -0
  20. package/dist/db/testdb.d.ts.map +1 -0
  21. package/dist/db/testdb.js +34 -0
  22. package/dist/dom/attrs.d.ts +67 -0
  23. package/dist/dom/attrs.d.ts.map +1 -0
  24. package/dist/dom/attrs.js +154 -0
  25. package/dist/dom/components.d.ts +43 -0
  26. package/dist/dom/components.d.ts.map +1 -0
  27. package/dist/dom/components.js +86 -0
  28. package/dist/dom/elems.d.ts +121 -0
  29. package/dist/dom/elems.d.ts.map +1 -0
  30. package/dist/dom/elems.js +52 -0
  31. package/dist/dom/events.d.ts +232 -0
  32. package/dist/dom/events.d.ts.map +1 -0
  33. package/dist/dom/events.js +289 -0
  34. package/dist/dom/index.d.ts +248 -0
  35. package/dist/dom/index.d.ts.map +1 -0
  36. package/dist/dom/index.js +266 -0
  37. package/dist/dom/nodeid.d.ts +9 -0
  38. package/dist/dom/nodeid.d.ts.map +1 -0
  39. package/dist/dom/nodeid.js +27 -0
  40. package/dist/dom/svg.d.ts +23 -0
  41. package/dist/dom/svg.d.ts.map +1 -0
  42. package/dist/dom/svg.js +1 -0
  43. package/dist/dom/views.d.ts +60 -0
  44. package/dist/dom/views.d.ts.map +1 -0
  45. package/dist/dom/views.js +154 -0
  46. package/dist/effects.d.ts +580 -0
  47. package/dist/effects.d.ts.map +1 -0
  48. package/dist/effects.js +1556 -0
  49. package/dist/index.d.ts +41 -0
  50. package/dist/index.d.ts.map +1 -0
  51. package/dist/index.js +78 -0
  52. package/dist/internal/clipboard.d.ts +5 -0
  53. package/dist/internal/clipboard.d.ts.map +1 -0
  54. package/dist/internal/clipboard.js +33 -0
  55. package/dist/internal/cmpmgr.d.ts +46 -0
  56. package/dist/internal/cmpmgr.d.ts.map +1 -0
  57. package/dist/internal/cmpmgr.js +561 -0
  58. package/dist/internal/dbmgr.d.ts +11 -0
  59. package/dist/internal/dbmgr.d.ts.map +1 -0
  60. package/dist/internal/dbmgr.js +200 -0
  61. package/dist/internal/dominterpreter.d.ts +5 -0
  62. package/dist/internal/dominterpreter.d.ts.map +1 -0
  63. package/dist/internal/dominterpreter.js +337 -0
  64. package/dist/internal/eventmgr.d.ts +41 -0
  65. package/dist/internal/eventmgr.d.ts.map +1 -0
  66. package/dist/internal/eventmgr.js +696 -0
  67. package/dist/internal/index.d.ts +19 -0
  68. package/dist/internal/index.d.ts.map +1 -0
  69. package/dist/internal/index.js +38 -0
  70. package/dist/internal/interpreter.d.ts +10 -0
  71. package/dist/internal/interpreter.d.ts.map +1 -0
  72. package/dist/internal/interpreter.js +1 -0
  73. package/dist/internal/locutils.d.ts +4 -0
  74. package/dist/internal/locutils.d.ts.map +1 -0
  75. package/dist/internal/locutils.js +22 -0
  76. package/dist/internal/net.d.ts +4 -0
  77. package/dist/internal/net.d.ts.map +1 -0
  78. package/dist/internal/net.js +64 -0
  79. package/dist/internal/realdom.d.ts +249 -0
  80. package/dist/internal/realdom.d.ts.map +1 -0
  81. package/dist/internal/realdom.js +1 -0
  82. package/dist/internal/styleregistry.d.ts +6 -0
  83. package/dist/internal/styleregistry.d.ts.map +1 -0
  84. package/dist/internal/styleregistry.js +18 -0
  85. package/dist/internal/taskreg.d.ts +9 -0
  86. package/dist/internal/taskreg.d.ts.map +1 -0
  87. package/dist/internal/taskreg.js +28 -0
  88. package/dist/internal/testing.d.ts +12 -0
  89. package/dist/internal/testing.d.ts.map +1 -0
  90. package/dist/internal/testing.js +974 -0
  91. package/dist/internal/testinginterpreter.d.ts +10 -0
  92. package/dist/internal/testinginterpreter.d.ts.map +1 -0
  93. package/dist/internal/testinginterpreter.js +646 -0
  94. package/dist/internal/vdom.d.ts +93 -0
  95. package/dist/internal/vdom.d.ts.map +1 -0
  96. package/dist/internal/vdom.js +470 -0
  97. package/dist/json/index.d.ts +5 -0
  98. package/dist/json/index.d.ts.map +1 -0
  99. package/dist/json/index.js +2 -0
  100. package/dist/json/validation.d.ts +28 -0
  101. package/dist/json/validation.d.ts.map +1 -0
  102. package/dist/json/validation.js +8 -0
  103. package/dist/net/fragment.d.ts +10 -0
  104. package/dist/net/fragment.d.ts.map +1 -0
  105. package/dist/net/fragment.js +27 -0
  106. package/dist/net/index.d.ts +42 -0
  107. package/dist/net/index.d.ts.map +1 -0
  108. package/dist/net/index.js +60 -0
  109. package/dist/net/location.d.ts +21 -0
  110. package/dist/net/location.d.ts.map +1 -0
  111. package/dist/net/location.js +41 -0
  112. package/dist/net/path.d.ts +12 -0
  113. package/dist/net/path.d.ts.map +1 -0
  114. package/dist/net/path.js +29 -0
  115. package/dist/net/queryparam.d.ts +10 -0
  116. package/dist/net/queryparam.d.ts.map +1 -0
  117. package/dist/net/queryparam.js +34 -0
  118. package/dist/router.d.ts +40 -0
  119. package/dist/router.d.ts.map +1 -0
  120. package/dist/router.js +77 -0
  121. package/dist/scroll.d.ts +7 -0
  122. package/dist/scroll.d.ts.map +1 -0
  123. package/dist/scroll.js +3 -0
  124. package/dist/testing.d.ts +248 -0
  125. package/dist/testing.d.ts.map +1 -0
  126. package/dist/testing.js +159 -0
  127. package/dist/utils/apply.d.ts +9 -0
  128. package/dist/utils/apply.d.ts.map +1 -0
  129. package/dist/utils/apply.js +10 -0
  130. package/dist/utils/index.d.ts +22 -0
  131. package/dist/utils/index.d.ts.map +1 -0
  132. package/dist/utils/index.js +138 -0
  133. package/dist/utils/lens.d.ts +18 -0
  134. package/dist/utils/lens.d.ts.map +1 -0
  135. package/dist/utils/lens.js +27 -0
  136. package/dist/utils/match.d.ts +17 -0
  137. package/dist/utils/match.d.ts.map +1 -0
  138. package/dist/utils/match.js +28 -0
  139. package/dist/utils/result.d.ts +47 -0
  140. package/dist/utils/result.d.ts.map +1 -0
  141. package/dist/utils/result.js +97 -0
  142. package/dist/webcomponent.d.ts +15 -0
  143. package/dist/webcomponent.d.ts.map +1 -0
  144. package/dist/webcomponent.js +43 -0
  145. package/package.json +1 -1
@@ -0,0 +1,10 @@
1
+ import { type TestConfig, type TestData } from '../testing';
2
+ import type { Interpreter } from './interpreter';
3
+ import type { RealWindow } from './realdom';
4
+ export declare function makeTestingInterpreter<Custom = {}>(initialData: TestData<Custom>, window: RealWindow, config?: TestConfig): {
5
+ interpreter: Interpreter;
6
+ getData: () => TestData<Custom>;
7
+ advanceTime: (ms: number) => void;
8
+ fireBroadcast: (channel: string, message: unknown) => void;
9
+ };
10
+ //# sourceMappingURL=testinginterpreter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"testinginterpreter.d.ts","sourceRoot":"","sources":["../../src/internal/testinginterpreter.ts"],"names":[],"mappings":"AA8BA,OAAO,EAAE,KAAK,UAAU,EAAE,KAAK,QAAQ,EAAE,MAAM,YAAY,CAAA;AAK3D,OAAO,KAAK,EAAa,WAAW,EAAE,MAAM,eAAe,CAAA;AAC3D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,WAAW,CAAA;AAwO3C,wBAAgB,sBAAsB,CAAC,MAAM,GAAG,EAAE,EAChD,WAAW,EAAE,QAAQ,CAAC,MAAM,CAAC,EAC7B,MAAM,EAAE,UAAU,EAClB,MAAM,CAAC,EAAE,UAAU,GAClB;IACD,WAAW,EAAE,WAAW,CAAA;IACxB,OAAO,EAAE,MAAM,QAAQ,CAAC,MAAM,CAAC,CAAA;IAC/B,WAAW,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAA;IACjC,aAAa,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,KAAK,IAAI,CAAA;CAC3D,CAyEA"}
@@ -0,0 +1,646 @@
1
+ // Deterministic hash-based PRNG for test fallback — never uses Math.random().
2
+ // Uses the 32-bit finalizer from MurmurHash3; distributes evenly over [0, 1).
3
+ function seededRandom(callIndex) {
4
+ let h = (callIndex + 1) | 0;
5
+ h = Math.imul(h ^ (h >>> 16), 0x45d9f3b);
6
+ h = Math.imul(h ^ (h >>> 16), 0x45d9f3b);
7
+ h ^= h >>> 16;
8
+ return (h >>> 0) / 0x100000000;
9
+ }
10
+ import { generateUuid } from '.';
11
+ import { permission, uuidFromString } from '..';
12
+ import { dbVersion } from '../db';
13
+ import { serializeIdbKey, TestDatabaseData, } from '../db/testdb';
14
+ import { taskId, } from '../effects';
15
+ import { scrollElementError } from '../scroll';
16
+ import { emptyLocation } from '../net/location';
17
+ import {} from '../testing';
18
+ import { exhaustivenessCheck } from '../utils';
19
+ import { run as runDbEffect } from '../db/effects';
20
+ import { run as runDbSetupEffect } from '../db/setup';
21
+ import { failure, success } from '../utils/result';
22
+ import { TaskRegistry } from './taskreg';
23
+ /* eslint-enable @typescript-eslint/no-explicit-any */
24
+ function snapshotTestData(acc) {
25
+ return {
26
+ window: {
27
+ alerts: [...acc.window.alerts],
28
+ confirms: [...acc.window.confirms],
29
+ confirmResponses: [...acc.window.confirmResponses],
30
+ prompts: [...acc.window.prompts],
31
+ promptResponses: [...acc.window.promptResponses],
32
+ },
33
+ navigator: { appBadgeOperations: [...acc.navigator.appBadgeOperations] },
34
+ clipboard: {
35
+ content: acc.clipboard.content,
36
+ writes: [...acc.clipboard.writes],
37
+ },
38
+ document: { title: acc.document.title },
39
+ http: { interactions: [...acc.http.interactions] },
40
+ clock: { calls: [...acc.clock.calls] },
41
+ random: { calls: [...acc.random.calls] },
42
+ console: { logs: [...acc.console.logs] },
43
+ db: new TestDatabaseData(new Map([...acc.db.databases.entries()].map(([dbName, db]) => [
44
+ dbName,
45
+ new Map([...db.stores.entries()].map(([storeName, store]) => [
46
+ storeName,
47
+ {
48
+ keyPath: store.keyPath,
49
+ records: new Map(store.records),
50
+ autoIncrementCounter: store.autoIncrementCounter,
51
+ },
52
+ ])),
53
+ ]))),
54
+ storage: {
55
+ local: new Map(acc.storage.local),
56
+ session: new Map(acc.storage.session),
57
+ },
58
+ navigation: {
59
+ locationHistory: [...acc.navigation.locationHistory],
60
+ },
61
+ scroll: {
62
+ windowScrolls: [...acc.scroll.windowScrolls],
63
+ elementScrolls: [...acc.scroll.elementScrolls],
64
+ },
65
+ downloads: [...acc.downloads],
66
+ broadcasts: {
67
+ sent: [...acc.broadcasts.sent],
68
+ },
69
+ custom: acc.custom,
70
+ };
71
+ }
72
+ class TestScheduler {
73
+ constructor() {
74
+ Object.defineProperty(this, "virtualNow", {
75
+ enumerable: true,
76
+ configurable: true,
77
+ writable: true,
78
+ value: 0
79
+ });
80
+ Object.defineProperty(this, "pending", {
81
+ enumerable: true,
82
+ configurable: true,
83
+ writable: true,
84
+ value: []
85
+ });
86
+ }
87
+ schedule(
88
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
89
+ task,
90
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
91
+ taskCallbacks, tskId, initialDelayInMillis, repeatIntervalInMillis) {
92
+ this.pending.push({
93
+ task,
94
+ taskCallbacks,
95
+ taskId: tskId,
96
+ nextFireAtMillis: this.virtualNow + initialDelayInMillis,
97
+ repeatIntervalInMillis,
98
+ });
99
+ }
100
+ cancel(tskId) {
101
+ this.pending = this.pending.filter(t => t.taskId !== tskId);
102
+ }
103
+ advance(ms, acc, broadcastReg, window) {
104
+ const targetTime = this.virtualNow + ms;
105
+ while (true) {
106
+ const next = this.pending
107
+ .filter(t => t.nextFireAtMillis <= targetTime)
108
+ .sort((a, b) => a.nextFireAtMillis - b.nextFireAtMillis)[0];
109
+ if (!next)
110
+ break;
111
+ this.virtualNow = next.nextFireAtMillis;
112
+ if (next.repeatIntervalInMillis !== undefined) {
113
+ next.nextFireAtMillis = this.virtualNow + next.repeatIntervalInMillis;
114
+ }
115
+ else {
116
+ this.pending = this.pending.filter(t => t !== next);
117
+ }
118
+ testingInterpreter(next.task, next.taskCallbacks, new TaskRegistry(), acc, broadcastReg, this, window);
119
+ }
120
+ this.virtualNow = targetTime;
121
+ }
122
+ }
123
+ class BroadcastChannelRegistry {
124
+ constructor() {
125
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
126
+ Object.defineProperty(this, "channels", {
127
+ enumerable: true,
128
+ configurable: true,
129
+ writable: true,
130
+ value: new Map()
131
+ });
132
+ }
133
+ subscribe(channel,
134
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
135
+ handler, tskId) {
136
+ if (!this.channels.has(channel))
137
+ this.channels.set(channel, []);
138
+ this.channels.get(channel).push({ handler, taskId: tskId });
139
+ }
140
+ unsubscribe(tskId) {
141
+ for (const [ch, subs] of this.channels) {
142
+ this.channels.set(ch, subs.filter(s => s.taskId !== tskId));
143
+ }
144
+ }
145
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
146
+ deliver(channel, message, acc, broadcastReg, scheduler, window) {
147
+ const subs = this.channels.get(channel) ?? [];
148
+ for (const sub of subs) {
149
+ testingInterpreter(sub.handler(message), {
150
+ onComplete() { },
151
+ onFireEvent() { },
152
+ getState() { return undefined; },
153
+ setState() { },
154
+ }, new TaskRegistry(), acc, broadcastReg, scheduler, window);
155
+ }
156
+ }
157
+ }
158
+ // eslint-disable-next-line @typescript-eslint/no-empty-object-type
159
+ export function makeTestingInterpreter(initialData, window, config) {
160
+ const acc = {
161
+ window: {
162
+ alerts: [...initialData.window.alerts],
163
+ confirms: [...initialData.window.confirms],
164
+ confirmResponses: [...initialData.window.confirmResponses],
165
+ prompts: [...initialData.window.prompts],
166
+ promptResponses: [...initialData.window.promptResponses],
167
+ },
168
+ navigator: {
169
+ appBadgeOperations: [...initialData.navigator.appBadgeOperations],
170
+ },
171
+ clipboard: {
172
+ content: initialData.clipboard.content,
173
+ writes: [...initialData.clipboard.writes],
174
+ },
175
+ document: { title: initialData.document.title },
176
+ http: { interactions: [...initialData.http.interactions] },
177
+ clock: { calls: [...initialData.clock.calls] },
178
+ random: { calls: [...initialData.random.calls] },
179
+ console: { logs: [...initialData.console.logs] },
180
+ storage: {
181
+ local: new Map(initialData.storage.local),
182
+ session: new Map(initialData.storage.session),
183
+ },
184
+ navigation: {
185
+ locationHistory: [...initialData.navigation.locationHistory],
186
+ },
187
+ scroll: {
188
+ windowScrolls: [...initialData.scroll.windowScrolls],
189
+ elementScrolls: [...initialData.scroll.elementScrolls],
190
+ },
191
+ downloads: [...initialData.downloads],
192
+ broadcasts: {
193
+ sent: [...initialData.broadcasts.sent],
194
+ },
195
+ db: {
196
+ databases: new Map([...initialData.db._databases.entries()].map(([dbName, dbSnap]) => [
197
+ dbName,
198
+ {
199
+ stores: new Map([...dbSnap.entries()].map(([storeName, storeSnap]) => [
200
+ storeName,
201
+ {
202
+ keyPath: storeSnap.keyPath,
203
+ records: new Map(storeSnap.records),
204
+ autoIncrementCounter: storeSnap.autoIncrementCounter,
205
+ },
206
+ ])),
207
+ },
208
+ ])),
209
+ },
210
+ uuidSequence: [],
211
+ custom: initialData.custom,
212
+ };
213
+ const scheduler = new TestScheduler();
214
+ const broadcastReg = new BroadcastChannelRegistry();
215
+ const interpreter = (effect, callbacks, taskRegistry) => {
216
+ testingInterpreter(effect, callbacks, taskRegistry, acc, broadcastReg, scheduler, window, config);
217
+ };
218
+ return {
219
+ interpreter,
220
+ getData: () => snapshotTestData(acc),
221
+ advanceTime: (ms) => scheduler.advance(ms, acc, broadcastReg, window),
222
+ fireBroadcast: (channel, message) => broadcastReg.deliver(channel, message, acc, broadcastReg, scheduler, window),
223
+ };
224
+ }
225
+ function testingInterpreter(effect, callbacks, taskRegistry, acc, broadcastReg, scheduler, window, config) {
226
+ switch (effect._type) {
227
+ case 'Return': {
228
+ callbacks.onComplete(effect.value);
229
+ return;
230
+ }
231
+ case 'FlatMap': {
232
+ testingInterpreter(effect.effect, {
233
+ ...callbacks,
234
+ onComplete(result) {
235
+ testingInterpreter(effect.next(result), callbacks, taskRegistry, acc, broadcastReg, scheduler, window, config);
236
+ },
237
+ }, taskRegistry, acc, broadcastReg, scheduler, window, config);
238
+ return;
239
+ }
240
+ case 'Suspend': {
241
+ switch (effect.operation._type) {
242
+ case 'Alert': {
243
+ acc.window.alerts.push(effect.operation.input);
244
+ callbacks.onComplete(undefined);
245
+ return;
246
+ }
247
+ case 'Confirm': {
248
+ const response = acc.window.confirmResponses.shift() ?? false;
249
+ acc.window.confirms.push({
250
+ message: effect.operation.input,
251
+ response,
252
+ });
253
+ callbacks.onComplete(response);
254
+ return;
255
+ }
256
+ case 'Prompt': {
257
+ const response = acc.window.promptResponses.shift() ?? null;
258
+ acc.window.prompts.push({
259
+ message: effect.operation.input.message,
260
+ default: effect.operation.input.default,
261
+ response,
262
+ });
263
+ callbacks.onComplete(response);
264
+ return;
265
+ }
266
+ case 'GetNotificationPermission': {
267
+ callbacks.onComplete(permission('default'));
268
+ return;
269
+ }
270
+ case 'RequestNotificationPermission': {
271
+ callbacks.onComplete(permission('granted'));
272
+ return;
273
+ }
274
+ case 'GetLocation': {
275
+ const history = acc.navigation.locationHistory;
276
+ callbacks.onComplete((history.length > 0
277
+ ? history[history.length - 1]
278
+ : emptyLocation));
279
+ return;
280
+ }
281
+ case 'Async': {
282
+ // run the inner effect synchronously, ignoring the delay
283
+ testingInterpreter(effect.operation.input.effect, {
284
+ ...callbacks,
285
+ onComplete() {
286
+ callbacks.onComplete(undefined);
287
+ },
288
+ }, taskRegistry, acc, broadcastReg, scheduler, window, config);
289
+ return;
290
+ }
291
+ case 'CancelTask': {
292
+ scheduler.cancel(effect.operation.input);
293
+ broadcastReg.unsubscribe(effect.operation.input);
294
+ taskRegistry.remove(effect.operation.input);
295
+ callbacks.onComplete(undefined);
296
+ return;
297
+ }
298
+ case 'ClearAppBadge': {
299
+ acc.navigator.appBadgeOperations.push({ _type: 'ClearAppBadge' });
300
+ callbacks.onComplete(undefined);
301
+ return;
302
+ }
303
+ case 'SetAppBadge': {
304
+ acc.navigator.appBadgeOperations.push({
305
+ _type: 'SetAppBadge',
306
+ count: effect.operation.input,
307
+ });
308
+ callbacks.onComplete(undefined);
309
+ return;
310
+ }
311
+ case 'SetDocumentTitle': {
312
+ acc.document.title = effect.operation.input;
313
+ callbacks.onComplete(undefined);
314
+ return;
315
+ }
316
+ case 'ReadClipboard': {
317
+ callbacks.onComplete(success(acc.clipboard.content));
318
+ return;
319
+ }
320
+ case 'WriteClipboard': {
321
+ acc.clipboard.content = effect.operation.input;
322
+ acc.clipboard.writes.push(effect.operation.input);
323
+ callbacks.onComplete(success(undefined));
324
+ return;
325
+ }
326
+ case 'Download': {
327
+ acc.downloads.push(effect.operation.input);
328
+ callbacks.onComplete(undefined);
329
+ return;
330
+ }
331
+ case 'PostBroadcastMessage': {
332
+ acc.broadcasts.sent.push(effect.operation.input);
333
+ callbacks.onComplete(undefined);
334
+ return;
335
+ }
336
+ case 'SubscribeToBroadcastChannel': {
337
+ const { channel, handler, taskId: tskIdInput } = effect.operation.input;
338
+ const tskId = tskIdInput ?? taskId(generateUuid());
339
+ if (tskIdInput) {
340
+ broadcastReg.unsubscribe(tskIdInput);
341
+ }
342
+ broadcastReg.subscribe(channel, handler, tskId);
343
+ callbacks.onComplete(tskId);
344
+ return;
345
+ }
346
+ case 'ScrollWindow': {
347
+ acc.scroll.windowScrolls.push(effect.operation.input);
348
+ callbacks.onComplete(undefined);
349
+ return;
350
+ }
351
+ case 'ScrollElement': {
352
+ const { selector, options } = effect.operation.input;
353
+ const el = window.document.querySelector(selector);
354
+ if (el === null) {
355
+ callbacks.onComplete(failure(scrollElementError(selector)));
356
+ }
357
+ else {
358
+ el.scrollTo(options);
359
+ acc.scroll.elementScrolls.push({ selector, options });
360
+ callbacks.onComplete(success(undefined));
361
+ }
362
+ return;
363
+ }
364
+ case 'FireEvent': {
365
+ callbacks.onFireEvent(effect.operation.input);
366
+ callbacks.onComplete(undefined);
367
+ return;
368
+ }
369
+ case 'GetState': {
370
+ callbacks.onComplete(callbacks.getState());
371
+ return;
372
+ }
373
+ case 'GetTime': {
374
+ const callIndex = acc.clock.calls.length;
375
+ const scripted = config?.clock.values ?? [];
376
+ const value = callIndex < scripted.length ? scripted[callIndex] : new Date(0);
377
+ acc.clock.calls.push(value);
378
+ callbacks.onComplete(value);
379
+ return;
380
+ }
381
+ case 'GetRandom': {
382
+ const callIndex = acc.random.calls.length;
383
+ const scripted = config?.random.values ?? [];
384
+ const value = callIndex < scripted.length
385
+ ? scripted[callIndex]
386
+ : seededRandom(callIndex);
387
+ acc.random.calls.push(value);
388
+ callbacks.onComplete(value);
389
+ return;
390
+ }
391
+ case 'Log': {
392
+ acc.console.logs.push(effect.operation.input);
393
+ callbacks.onComplete(undefined);
394
+ return;
395
+ }
396
+ case 'MakeHttpRequest': {
397
+ // TODO: add HTTP mock support for testing
398
+ callbacks.onComplete(undefined);
399
+ return;
400
+ }
401
+ case 'Product': {
402
+ let resultA = '__ctrl_fx_unset_result';
403
+ let resultB = '__ctrl_fx_unset_result';
404
+ testingInterpreter(effect.operation.input[0], {
405
+ ...callbacks,
406
+ onComplete(a) {
407
+ resultA = a;
408
+ if (resultB !== '__ctrl_fx_unset_result') {
409
+ callbacks.onComplete([resultA, resultB]);
410
+ }
411
+ },
412
+ }, taskRegistry, acc, broadcastReg, scheduler, window, config);
413
+ testingInterpreter(effect.operation.input[1], {
414
+ ...callbacks,
415
+ onComplete(b) {
416
+ resultB = b;
417
+ if (resultA !== '__ctrl_fx_unset_result') {
418
+ callbacks.onComplete([resultA, resultB]);
419
+ }
420
+ },
421
+ }, taskRegistry, acc, broadcastReg, scheduler, window, config);
422
+ return;
423
+ }
424
+ case 'ScheduleTask': {
425
+ const tskId = effect.operation.input.taskId ?? taskId(generateUuid());
426
+ // rescheduling: cancel any existing task with this id
427
+ if (effect.operation.input.taskId) {
428
+ scheduler.cancel(effect.operation.input.taskId);
429
+ }
430
+ scheduler.schedule(
431
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
432
+ effect.operation.input.task,
433
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
434
+ { ...callbacks, onComplete() { } }, tskId, effect.operation.input.initialDelayInMillis, effect.operation.input.repeatIntervalInMillis);
435
+ callbacks.onComplete(tskId);
436
+ return;
437
+ }
438
+ case 'SetTimeout': {
439
+ callbacks.onComplete(undefined);
440
+ return;
441
+ }
442
+ case 'PushState': {
443
+ acc.navigation.locationHistory.push(effect.operation.input);
444
+ callbacks.onComplete(undefined);
445
+ return;
446
+ }
447
+ case 'ReplaceState': {
448
+ if (acc.navigation.locationHistory.length > 0) {
449
+ acc.navigation.locationHistory[acc.navigation.locationHistory.length - 1] = effect.operation.input;
450
+ }
451
+ else {
452
+ acc.navigation.locationHistory.push(effect.operation.input);
453
+ }
454
+ callbacks.onComplete(undefined);
455
+ return;
456
+ }
457
+ case 'Go': {
458
+ const delta = effect.operation.input;
459
+ const newLen = acc.navigation.locationHistory.length + delta;
460
+ if (newLen > 0) {
461
+ acc.navigation.locationHistory.length = newLen;
462
+ }
463
+ callbacks.onComplete(undefined);
464
+ return;
465
+ }
466
+ case 'GenerateUuid': {
467
+ const uuidLen = acc.uuidSequence.length;
468
+ const nextUuid = uuidFromString(`${uuidLen}`);
469
+ acc.uuidSequence.push(nextUuid);
470
+ callbacks.onComplete(nextUuid);
471
+ return;
472
+ }
473
+ case 'GetStorageItem': {
474
+ const store = acc.storage[effect.operation.input.storage];
475
+ callbacks.onComplete((store.get(effect.operation.input.key) ?? null));
476
+ return;
477
+ }
478
+ case 'SetStorageItem': {
479
+ acc.storage[effect.operation.input.storage].set(effect.operation.input.key, effect.operation.input.value);
480
+ callbacks.onComplete(undefined);
481
+ return;
482
+ }
483
+ case 'RemoveStorageItem': {
484
+ acc.storage[effect.operation.input.storage].delete(effect.operation.input.key);
485
+ callbacks.onComplete(undefined);
486
+ return;
487
+ }
488
+ case 'ClearStorage': {
489
+ acc.storage[effect.operation.input.storage].clear();
490
+ callbacks.onComplete(undefined);
491
+ return;
492
+ }
493
+ case 'OpenDatabase': {
494
+ const { db, setup } = effect.operation.input;
495
+ if (!acc.db.databases.has(db)) {
496
+ const mutableDb = { stores: new Map() };
497
+ acc.db.databases.set(db, mutableDb);
498
+ runDbSetupEffect(setup(dbVersion(0)), makeSetupInterpreter(mutableDb), _r => {
499
+ callbacks.onComplete(success(undefined));
500
+ });
501
+ }
502
+ else {
503
+ callbacks.onComplete(success(undefined));
504
+ }
505
+ return;
506
+ }
507
+ case 'RunDbTransaction': {
508
+ const { db, effect: dbEffect } = effect.operation.input;
509
+ const mutableDb = acc.db.databases.get(db);
510
+ if (!mutableDb) {
511
+ callbacks.onComplete(failure({
512
+ _type: 'UnexpectedError',
513
+ message: `Database '${db}' is not open`,
514
+ }));
515
+ return;
516
+ }
517
+ runDbEffect(dbEffect, makeDbInterpreter(mutableDb), _r => {
518
+ callbacks.onComplete(success(undefined));
519
+ });
520
+ return;
521
+ }
522
+ case 'UpdateState': {
523
+ callbacks.setState(effect.operation.input(callbacks.getState()));
524
+ callbacks.onComplete(undefined);
525
+ return;
526
+ }
527
+ case 'CustomEffect': {
528
+ const { args, test } = effect.operation.input;
529
+ const result = test(args, acc.custom);
530
+ acc.custom = result.data;
531
+ callbacks.onComplete(result.out);
532
+ return;
533
+ }
534
+ default: {
535
+ exhaustivenessCheck(effect.operation);
536
+ }
537
+ }
538
+ break;
539
+ }
540
+ default: {
541
+ exhaustivenessCheck(effect);
542
+ }
543
+ }
544
+ }
545
+ function makeSetupInterpreter(mutableDb) {
546
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
547
+ return (op, onComplete) => {
548
+ switch (op._type) {
549
+ case 'CreateObjectStore': {
550
+ mutableDb.stores.set(op.objectStore, {
551
+ keyPath: op.keyPath,
552
+ records: new Map(),
553
+ autoIncrementCounter: 1,
554
+ });
555
+ onComplete(undefined);
556
+ return;
557
+ }
558
+ case 'CreateIndex': {
559
+ onComplete(undefined);
560
+ return;
561
+ }
562
+ case 'DeleteObjectStore': {
563
+ mutableDb.stores.delete(op.objectStore);
564
+ onComplete(undefined);
565
+ return;
566
+ }
567
+ case 'DeleteIndex': {
568
+ onComplete(undefined);
569
+ return;
570
+ }
571
+ case 'GetObjectStoreNames': {
572
+ onComplete([...mutableDb.stores.keys()]);
573
+ return;
574
+ }
575
+ case 'GetIndexNames': {
576
+ onComplete([]);
577
+ return;
578
+ }
579
+ case 'RunDbEffect': {
580
+ runDbEffect(op.effect, makeDbInterpreter(mutableDb), _r => onComplete(undefined));
581
+ return;
582
+ }
583
+ }
584
+ };
585
+ }
586
+ function makeDbInterpreter(mutableDb) {
587
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
588
+ return (op, onComplete) => {
589
+ switch (op._type) {
590
+ case 'Add':
591
+ case 'Put': {
592
+ const store = requireStore(mutableDb, op.objectStore);
593
+ store.records.set(extractKey(op.value, store), op.value);
594
+ onComplete(undefined);
595
+ return;
596
+ }
597
+ case 'Get': {
598
+ const store = requireStore(mutableDb, op.objectStore);
599
+ onComplete(store.records.get(serializeIdbKey(op.key)));
600
+ return;
601
+ }
602
+ case 'GetAll': {
603
+ const store = requireStore(mutableDb, op.objectStore);
604
+ onComplete([...store.records.values()]);
605
+ return;
606
+ }
607
+ case 'Delete': {
608
+ const store = requireStore(mutableDb, op.objectStore);
609
+ store.records.delete(serializeIdbKey(op.key));
610
+ onComplete(undefined);
611
+ return;
612
+ }
613
+ case 'Count': {
614
+ const store = requireStore(mutableDb, op.objectStore);
615
+ onComplete(store.records.size);
616
+ return;
617
+ }
618
+ }
619
+ };
620
+ }
621
+ function requireStore(mutableDb, storeName) {
622
+ const store = mutableDb.stores.get(storeName);
623
+ if (!store)
624
+ throw new Error(`Object store '${storeName}' not found`);
625
+ return store;
626
+ }
627
+ function extractKey(value, store) {
628
+ const { keyPath } = store;
629
+ switch (keyPath._type) {
630
+ case 'AutoIncrement':
631
+ return String(store.autoIncrementCounter++);
632
+ case 'Manual': {
633
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
634
+ throw new Error(`Cannot extract key '${keyPath.value}' from non-object`);
635
+ }
636
+ return JSON.stringify(value[keyPath.value]);
637
+ }
638
+ case 'Multi': {
639
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
640
+ throw new Error('Cannot extract compound key from non-object');
641
+ }
642
+ const obj = value;
643
+ return JSON.stringify([keyPath.first, keyPath.second, ...keyPath.rest].map(f => obj[f]));
644
+ }
645
+ }
646
+ }