@powersync/service-core 1.12.1 → 1.13.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 (78) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/api/RouteAPI.d.ts +1 -1
  3. package/dist/api/diagnostics.js +1 -1
  4. package/dist/api/diagnostics.js.map +1 -1
  5. package/dist/entry/cli-entry.js +2 -2
  6. package/dist/entry/cli-entry.js.map +1 -1
  7. package/dist/index.d.ts +1 -0
  8. package/dist/index.js +1 -0
  9. package/dist/index.js.map +1 -1
  10. package/dist/metrics/open-telemetry/OpenTelemetryMetricsFactory.d.ts +1 -1
  11. package/dist/metrics/open-telemetry/OpenTelemetryMetricsFactory.js.map +1 -1
  12. package/dist/replication/AbstractReplicationJob.d.ts +4 -0
  13. package/dist/replication/AbstractReplicationJob.js.map +1 -1
  14. package/dist/replication/AbstractReplicator.d.ts +25 -1
  15. package/dist/replication/AbstractReplicator.js +53 -3
  16. package/dist/replication/AbstractReplicator.js.map +1 -1
  17. package/dist/replication/RelationCache.d.ts +9 -0
  18. package/dist/replication/RelationCache.js +20 -0
  19. package/dist/replication/RelationCache.js.map +1 -0
  20. package/dist/replication/replication-index.d.ts +1 -0
  21. package/dist/replication/replication-index.js +1 -0
  22. package/dist/replication/replication-index.js.map +1 -1
  23. package/dist/replication/replication-metrics.js +6 -0
  24. package/dist/replication/replication-metrics.js.map +1 -1
  25. package/dist/routes/endpoints/admin.js +2 -0
  26. package/dist/routes/endpoints/admin.js.map +1 -1
  27. package/dist/storage/BucketStorageBatch.d.ts +21 -3
  28. package/dist/storage/BucketStorageBatch.js +2 -1
  29. package/dist/storage/BucketStorageBatch.js.map +1 -1
  30. package/dist/storage/PersistedSyncRulesContent.d.ts +5 -0
  31. package/dist/storage/SourceTable.d.ts +17 -1
  32. package/dist/storage/SourceTable.js +28 -0
  33. package/dist/storage/SourceTable.js.map +1 -1
  34. package/dist/storage/StorageEngine.d.ts +3 -2
  35. package/dist/storage/StorageEngine.js +3 -0
  36. package/dist/storage/StorageEngine.js.map +1 -1
  37. package/dist/storage/StorageProvider.d.ts +2 -0
  38. package/dist/storage/SyncRulesBucketStorage.d.ts +18 -6
  39. package/dist/storage/SyncRulesBucketStorage.js.map +1 -1
  40. package/dist/storage/WriteCheckpointAPI.d.ts +0 -26
  41. package/dist/storage/WriteCheckpointAPI.js.map +1 -1
  42. package/dist/storage/bson.js +4 -1
  43. package/dist/storage/bson.js.map +1 -1
  44. package/dist/streams/BroadcastIterable.d.ts +1 -1
  45. package/dist/streams/streams-index.d.ts +0 -1
  46. package/dist/streams/streams-index.js +0 -1
  47. package/dist/streams/streams-index.js.map +1 -1
  48. package/dist/system/ServiceContext.js +6 -0
  49. package/dist/system/ServiceContext.js.map +1 -1
  50. package/package.json +4 -4
  51. package/src/api/RouteAPI.ts +1 -1
  52. package/src/api/diagnostics.ts +1 -1
  53. package/src/entry/cli-entry.ts +2 -2
  54. package/src/index.ts +2 -0
  55. package/src/metrics/open-telemetry/OpenTelemetryMetricsFactory.ts +3 -3
  56. package/src/replication/AbstractReplicationJob.ts +5 -0
  57. package/src/replication/AbstractReplicator.ts +56 -3
  58. package/src/replication/RelationCache.ts +25 -0
  59. package/src/replication/replication-index.ts +1 -0
  60. package/src/replication/replication-metrics.ts +7 -0
  61. package/src/routes/endpoints/admin.ts +2 -0
  62. package/src/storage/BucketStorageBatch.ts +26 -4
  63. package/src/storage/PersistedSyncRulesContent.ts +6 -0
  64. package/src/storage/SourceTable.ts +44 -1
  65. package/src/storage/StorageEngine.ts +6 -2
  66. package/src/storage/StorageProvider.ts +3 -0
  67. package/src/storage/SyncRulesBucketStorage.ts +22 -6
  68. package/src/storage/WriteCheckpointAPI.ts +0 -30
  69. package/src/storage/bson.ts +4 -1
  70. package/src/streams/BroadcastIterable.ts +1 -1
  71. package/src/streams/streams-index.ts +0 -1
  72. package/src/system/ServiceContext.ts +6 -0
  73. package/tsconfig.tsbuildinfo +1 -1
  74. package/dist/streams/Demultiplexer.d.ts +0 -52
  75. package/dist/streams/Demultiplexer.js +0 -128
  76. package/dist/streams/Demultiplexer.js.map +0 -1
  77. package/src/streams/Demultiplexer.ts +0 -165
  78. package/test/src/demultiplexer.test.ts +0 -205
@@ -1,52 +0,0 @@
1
- export interface DemultiplexerValue<T> {
2
- /**
3
- * The key used for demultiplexing, for example the user id.
4
- */
5
- key: string;
6
- /**
7
- * The stream value.
8
- */
9
- value: T;
10
- }
11
- export interface DemultiplexerSource<T> {
12
- /**
13
- * The async iterator providing a stream of values.
14
- */
15
- iterator: AsyncIterable<DemultiplexerValue<T>>;
16
- /**
17
- * Fetches the first value for a given key.
18
- *
19
- * This is used to get an initial value for each subscription.
20
- */
21
- getFirstValue(key: string): Promise<T>;
22
- }
23
- export type DemultiplexerSourceFactory<T> = (signal: AbortSignal) => DemultiplexerSource<T>;
24
- /**
25
- * Takes a multiplexed stream (e.g. a changestream covering many individual users),
26
- * and allows subscribing to individual streams.
27
- *
28
- * The source subscription is lazy:
29
- * 1. We only start subscribing when there is a downstream subscriber.
30
- * 2. When all downstream subscriptions have ended, we end the source subscription.
31
- *
32
- * For each subscriber, if backpressure builds up, we only keep the _last_ value.
33
- */
34
- export declare class Demultiplexer<T> {
35
- private source;
36
- private subscribers;
37
- private abortController;
38
- private currentSource;
39
- constructor(source: DemultiplexerSourceFactory<T>);
40
- private start;
41
- private loop;
42
- private removeSink;
43
- private addSink;
44
- /**
45
- * Subscribe to a specific stream.
46
- *
47
- * @param key The key used for demultiplexing, e.g. user id.
48
- * @param signal
49
- */
50
- subscribe(key: string, signal: AbortSignal): AsyncIterable<T>;
51
- get active(): boolean;
52
- }
@@ -1,128 +0,0 @@
1
- import { AbortError } from 'ix/aborterror.js';
2
- import { LastValueSink } from './LastValueSink.js';
3
- /**
4
- * Takes a multiplexed stream (e.g. a changestream covering many individual users),
5
- * and allows subscribing to individual streams.
6
- *
7
- * The source subscription is lazy:
8
- * 1. We only start subscribing when there is a downstream subscriber.
9
- * 2. When all downstream subscriptions have ended, we end the source subscription.
10
- *
11
- * For each subscriber, if backpressure builds up, we only keep the _last_ value.
12
- */
13
- export class Demultiplexer {
14
- source;
15
- subscribers = undefined;
16
- abortController = undefined;
17
- currentSource = undefined;
18
- constructor(source) {
19
- this.source = source;
20
- }
21
- start(filter, sink) {
22
- const abortController = new AbortController();
23
- const listeners = new Map();
24
- listeners.set(filter, new Set([sink]));
25
- this.abortController = abortController;
26
- this.subscribers = listeners;
27
- const source = this.source(abortController.signal);
28
- this.currentSource = source;
29
- this.loop(source, abortController, listeners);
30
- return source;
31
- }
32
- async loop(source, abortController, sinks) {
33
- try {
34
- for await (let doc of source.iterator) {
35
- if (abortController.signal.aborted || sinks.size == 0) {
36
- throw new AbortError();
37
- }
38
- const key = doc.key;
39
- const keySinks = sinks.get(key);
40
- if (keySinks == null) {
41
- continue;
42
- }
43
- for (let sink of keySinks) {
44
- sink.write(doc.value);
45
- }
46
- }
47
- // End of stream
48
- for (let keySinks of sinks.values()) {
49
- for (let sink of keySinks) {
50
- sink.end();
51
- }
52
- }
53
- }
54
- catch (e) {
55
- // Just in case the error is not from the source
56
- abortController.abort();
57
- for (let keySinks of sinks.values()) {
58
- for (let sink of keySinks) {
59
- sink.error(e);
60
- }
61
- }
62
- }
63
- finally {
64
- // Clear state, so that a new subscription may be started
65
- if (this.subscribers === sinks) {
66
- this.subscribers = undefined;
67
- this.abortController = undefined;
68
- this.currentSource = undefined;
69
- }
70
- }
71
- }
72
- removeSink(key, sink) {
73
- const existing = this.subscribers?.get(key);
74
- if (existing == null) {
75
- return;
76
- }
77
- existing.delete(sink);
78
- if (existing.size == 0) {
79
- this.subscribers.delete(key);
80
- }
81
- if (this.subscribers?.size == 0) {
82
- // This is not immediate - there may be a delay until it is fully stopped,
83
- // depending on the underlying source.
84
- this.abortController?.abort();
85
- this.subscribers = undefined;
86
- this.abortController = undefined;
87
- this.currentSource = undefined;
88
- }
89
- }
90
- addSink(key, sink) {
91
- if (this.currentSource == null) {
92
- return this.start(key, sink);
93
- }
94
- else {
95
- const existing = this.subscribers.get(key);
96
- if (existing != null) {
97
- existing.add(sink);
98
- }
99
- else {
100
- this.subscribers.set(key, new Set([sink]));
101
- }
102
- return this.currentSource;
103
- }
104
- }
105
- /**
106
- * Subscribe to a specific stream.
107
- *
108
- * @param key The key used for demultiplexing, e.g. user id.
109
- * @param signal
110
- */
111
- async *subscribe(key, signal) {
112
- const sink = new LastValueSink(undefined);
113
- // Important that we register the sink before calling getFirstValue().
114
- const source = this.addSink(key, sink);
115
- try {
116
- const firstValue = await source.getFirstValue(key);
117
- yield firstValue;
118
- yield* sink.withSignal(signal);
119
- }
120
- finally {
121
- this.removeSink(key, sink);
122
- }
123
- }
124
- get active() {
125
- return this.subscribers != null;
126
- }
127
- }
128
- //# sourceMappingURL=Demultiplexer.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"Demultiplexer.js","sourceRoot":"","sources":["../../src/streams/Demultiplexer.ts"],"names":[],"mappings":"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"}
@@ -1,165 +0,0 @@
1
- import { AbortError } from 'ix/aborterror.js';
2
- import { wrapWithAbort } from 'ix/asynciterable/operators/withabort.js';
3
- import { LastValueSink } from './LastValueSink.js';
4
-
5
- export interface DemultiplexerValue<T> {
6
- /**
7
- * The key used for demultiplexing, for example the user id.
8
- */
9
- key: string;
10
- /**
11
- * The stream value.
12
- */
13
- value: T;
14
- }
15
-
16
- export interface DemultiplexerSource<T> {
17
- /**
18
- * The async iterator providing a stream of values.
19
- */
20
- iterator: AsyncIterable<DemultiplexerValue<T>>;
21
-
22
- /**
23
- * Fetches the first value for a given key.
24
- *
25
- * This is used to get an initial value for each subscription.
26
- */
27
- getFirstValue(key: string): Promise<T>;
28
- }
29
-
30
- export type DemultiplexerSourceFactory<T> = (signal: AbortSignal) => DemultiplexerSource<T>;
31
-
32
- /**
33
- * Takes a multiplexed stream (e.g. a changestream covering many individual users),
34
- * and allows subscribing to individual streams.
35
- *
36
- * The source subscription is lazy:
37
- * 1. We only start subscribing when there is a downstream subscriber.
38
- * 2. When all downstream subscriptions have ended, we end the source subscription.
39
- *
40
- * For each subscriber, if backpressure builds up, we only keep the _last_ value.
41
- */
42
- export class Demultiplexer<T> {
43
- private subscribers: Map<string, Set<LastValueSink<T>>> | undefined = undefined;
44
- private abortController: AbortController | undefined = undefined;
45
- private currentSource: DemultiplexerSource<T> | undefined = undefined;
46
-
47
- constructor(private source: DemultiplexerSourceFactory<T>) {}
48
-
49
- private start(filter: string, sink: LastValueSink<T>) {
50
- const abortController = new AbortController();
51
- const listeners = new Map();
52
- listeners.set(filter, new Set([sink]));
53
-
54
- this.abortController = abortController;
55
- this.subscribers = listeners;
56
-
57
- const source = this.source(abortController.signal);
58
- this.currentSource = source;
59
- this.loop(source, abortController, listeners);
60
- return source;
61
- }
62
-
63
- private async loop(
64
- source: DemultiplexerSource<T>,
65
- abortController: AbortController,
66
- sinks: Map<string, Set<LastValueSink<T>>>
67
- ) {
68
- try {
69
- for await (let doc of source.iterator) {
70
- if (abortController.signal.aborted || sinks.size == 0) {
71
- throw new AbortError();
72
- }
73
- const key = doc.key;
74
- const keySinks = sinks.get(key);
75
- if (keySinks == null) {
76
- continue;
77
- }
78
-
79
- for (let sink of keySinks) {
80
- sink.write(doc.value);
81
- }
82
- }
83
-
84
- // End of stream
85
- for (let keySinks of sinks.values()) {
86
- for (let sink of keySinks) {
87
- sink.end();
88
- }
89
- }
90
- } catch (e) {
91
- // Just in case the error is not from the source
92
- abortController.abort();
93
-
94
- for (let keySinks of sinks.values()) {
95
- for (let sink of keySinks) {
96
- sink.error(e);
97
- }
98
- }
99
- } finally {
100
- // Clear state, so that a new subscription may be started
101
- if (this.subscribers === sinks) {
102
- this.subscribers = undefined;
103
- this.abortController = undefined;
104
- this.currentSource = undefined;
105
- }
106
- }
107
- }
108
-
109
- private removeSink(key: string, sink: LastValueSink<T>) {
110
- const existing = this.subscribers?.get(key);
111
- if (existing == null) {
112
- return;
113
- }
114
- existing.delete(sink);
115
- if (existing.size == 0) {
116
- this.subscribers!.delete(key);
117
- }
118
-
119
- if (this.subscribers?.size == 0) {
120
- // This is not immediate - there may be a delay until it is fully stopped,
121
- // depending on the underlying source.
122
- this.abortController?.abort();
123
- this.subscribers = undefined;
124
- this.abortController = undefined;
125
- this.currentSource = undefined;
126
- }
127
- }
128
-
129
- private addSink(key: string, sink: LastValueSink<T>) {
130
- if (this.currentSource == null) {
131
- return this.start(key, sink);
132
- } else {
133
- const existing = this.subscribers!.get(key);
134
- if (existing != null) {
135
- existing.add(sink);
136
- } else {
137
- this.subscribers!.set(key, new Set([sink]));
138
- }
139
- return this.currentSource;
140
- }
141
- }
142
-
143
- /**
144
- * Subscribe to a specific stream.
145
- *
146
- * @param key The key used for demultiplexing, e.g. user id.
147
- * @param signal
148
- */
149
- async *subscribe(key: string, signal: AbortSignal): AsyncIterable<T> {
150
- const sink = new LastValueSink<T>(undefined);
151
- // Important that we register the sink before calling getFirstValue().
152
- const source = this.addSink(key, sink);
153
- try {
154
- const firstValue = await source.getFirstValue(key);
155
- yield firstValue;
156
- yield* sink.withSignal(signal);
157
- } finally {
158
- this.removeSink(key, sink);
159
- }
160
- }
161
-
162
- get active() {
163
- return this.subscribers != null;
164
- }
165
- }
@@ -1,205 +0,0 @@
1
- // Vitest Unit Tests
2
- import { Demultiplexer, DemultiplexerSource, DemultiplexerSourceFactory, DemultiplexerValue } from '@/index.js';
3
- import { delayEach } from 'ix/asynciterable/operators/delayeach.js';
4
- import { take } from 'ix/asynciterable/operators/take.js';
5
- import { toArray } from 'ix/asynciterable/toarray.js';
6
- import * as timers from 'node:timers/promises';
7
- import { describe, expect, it } from 'vitest';
8
-
9
- describe('Demultiplexer', () => {
10
- it('should start subscription lazily and provide first value', async () => {
11
- const mockSource: DemultiplexerSourceFactory<string> = (signal: AbortSignal) => {
12
- const iterator = (async function* (): AsyncIterable<DemultiplexerValue<string>> {})();
13
- return {
14
- iterator,
15
- getFirstValue: async (key: string) => `first-${key}`
16
- };
17
- };
18
-
19
- const demux = new Demultiplexer(mockSource);
20
- const signal = new AbortController().signal;
21
-
22
- const iter = demux.subscribe('user1', signal)[Symbol.asyncIterator]();
23
- const result = await iter.next();
24
- expect(result.value).toBe('first-user1');
25
- });
26
-
27
- it('should handle multiple subscribers to the same key', async () => {
28
- const iter = (async function* () {
29
- yield { key: 'user1', value: 'value1' };
30
- yield { key: 'user1', value: 'value2' };
31
- })();
32
- const source: DemultiplexerSource<string> = {
33
- iterator: iter,
34
- getFirstValue: async (key: string) => `first-${key}`
35
- };
36
-
37
- const demux = new Demultiplexer(() => source);
38
- const signal = new AbortController().signal;
39
-
40
- const iter1 = demux.subscribe('user1', signal)[Symbol.asyncIterator]();
41
- const iter2 = demux.subscribe('user1', signal)[Symbol.asyncIterator]();
42
-
43
- // Due to only keeping the last value, some values are skipped
44
- expect(await iter1.next()).toEqual({ value: 'first-user1', done: false });
45
- expect(await iter1.next()).toEqual({ value: 'value1', done: false });
46
- expect(await iter1.next()).toEqual({ value: undefined, done: true });
47
-
48
- expect(await iter2.next()).toEqual({ value: 'first-user1', done: false });
49
- expect(await iter2.next()).toEqual({ value: undefined, done: true });
50
- });
51
-
52
- it('should handle multiple subscribers to the same key (2)', async () => {
53
- const p1 = Promise.withResolvers<void>();
54
- const p2 = Promise.withResolvers<void>();
55
- const p3 = Promise.withResolvers<void>();
56
-
57
- const iter = (async function* () {
58
- await p1.promise;
59
- yield { key: 'user1', value: 'value1' };
60
- await p2.promise;
61
- yield { key: 'user1', value: 'value2' };
62
- await p3.promise;
63
- })();
64
-
65
- const source: DemultiplexerSource<string> = {
66
- iterator: iter,
67
- getFirstValue: async (key: string) => `first-${key}`
68
- };
69
-
70
- const demux = new Demultiplexer(() => source);
71
- const signal = new AbortController().signal;
72
-
73
- const iter1 = demux.subscribe('user1', signal)[Symbol.asyncIterator]();
74
- const iter2 = demux.subscribe('user1', signal)[Symbol.asyncIterator]();
75
-
76
- // Due to only keeping the last value, some values are skilled
77
- expect(await iter1.next()).toEqual({ value: 'first-user1', done: false });
78
- expect(await iter2.next()).toEqual({ value: 'first-user1', done: false });
79
- p1.resolve();
80
-
81
- expect(await iter1.next()).toEqual({ value: 'value1', done: false });
82
- expect(await iter2.next()).toEqual({ value: 'value1', done: false });
83
- p2.resolve();
84
-
85
- expect(await iter1.next()).toEqual({ value: 'value2', done: false });
86
- p3.resolve();
87
-
88
- expect(await iter1.next()).toEqual({ value: undefined, done: true });
89
- expect(await iter2.next()).toEqual({ value: undefined, done: true });
90
- });
91
-
92
- it('should handle multiple subscribers to different keys', async () => {
93
- const p1 = Promise.withResolvers<void>();
94
- const p2 = Promise.withResolvers<void>();
95
- const p3 = Promise.withResolvers<void>();
96
-
97
- const iter = (async function* () {
98
- await p1.promise;
99
- yield { key: 'user1', value: 'value1' };
100
- await p2.promise;
101
- yield { key: 'user2', value: 'value2' };
102
- await p3.promise;
103
- })();
104
-
105
- const source: DemultiplexerSource<string> = {
106
- iterator: iter,
107
- getFirstValue: async (key: string) => `first-${key}`
108
- };
109
-
110
- const demux = new Demultiplexer(() => source);
111
- const signal = new AbortController().signal;
112
-
113
- const iter1 = demux.subscribe('user1', signal)[Symbol.asyncIterator]();
114
- const iter2 = demux.subscribe('user2', signal)[Symbol.asyncIterator]();
115
-
116
- // Due to only keeping the last value, some values are skilled
117
- expect(await iter1.next()).toEqual({ value: 'first-user1', done: false });
118
- expect(await iter2.next()).toEqual({ value: 'first-user2', done: false });
119
- p1.resolve();
120
-
121
- expect(await iter1.next()).toEqual({ value: 'value1', done: false });
122
- p2.resolve();
123
-
124
- expect(await iter2.next()).toEqual({ value: 'value2', done: false });
125
- p3.resolve();
126
-
127
- expect(await iter1.next()).toEqual({ value: undefined, done: true });
128
- expect(await iter2.next()).toEqual({ value: undefined, done: true });
129
- });
130
-
131
- it('should abort', async () => {
132
- const iter = (async function* () {
133
- yield { key: 'user1', value: 'value1' };
134
- yield { key: 'user1', value: 'value2' };
135
- })();
136
-
137
- const source: DemultiplexerSource<string> = {
138
- iterator: iter,
139
- getFirstValue: async (key: string) => `first-${key}`
140
- };
141
-
142
- const demux = new Demultiplexer(() => source);
143
- const controller = new AbortController();
144
-
145
- const iter1 = demux.subscribe('user1', controller.signal)[Symbol.asyncIterator]();
146
-
147
- expect(await iter1.next()).toEqual({ value: 'first-user1', done: false });
148
- controller.abort();
149
-
150
- await expect(iter1.next()).rejects.toThrow('The operation has been aborted');
151
- });
152
-
153
- it('should handle errors on multiple subscribers', async () => {
154
- let sourceIndex = 0;
155
- const sourceFn = async function* (signal: AbortSignal): AsyncIterable<DemultiplexerValue<number>> {
156
- // Test value out by 1000 means it may have used the wrong iteration of the source
157
- const base = (sourceIndex += 1000);
158
- const abortedPromise = new Promise((resolve) => {
159
- signal.addEventListener('abort', resolve, { once: true });
160
- });
161
- for (let i = 0; !signal.aborted; i++) {
162
- if (base + i == 1005) {
163
- throw new Error('simulated failure');
164
- }
165
- yield { key: 'u1', value: base + i };
166
- await Promise.race([abortedPromise, timers.setTimeout(1)]);
167
- }
168
- // Test value out by 100 means this wasn't reached
169
- sourceIndex += 100;
170
- };
171
-
172
- const sourceFactory: DemultiplexerSourceFactory<number> = (signal) => {
173
- const source: DemultiplexerSource<number> = {
174
- iterator: sourceFn(signal),
175
- getFirstValue: async (key: string) => -1
176
- };
177
- return source;
178
- };
179
- const demux = new Demultiplexer(sourceFactory);
180
-
181
- const controller = new AbortController();
182
-
183
- const delayed1 = delayEach(9)(demux.subscribe('u1', controller.signal));
184
- const delayed2 = delayEach(10)(demux.subscribe('u1', controller.signal));
185
- expect(demux.active).toBe(false);
186
- const results1Promise = toArray(take(5)(delayed1)) as Promise<number[]>;
187
- const results2Promise = toArray(take(5)(delayed2)) as Promise<number[]>;
188
-
189
- const [r1, r2] = await Promise.allSettled([results1Promise, results2Promise]);
190
-
191
- expect(r1).toEqual({ status: 'rejected', reason: new Error('simulated failure') });
192
- expect(r2).toEqual({ status: 'rejected', reason: new Error('simulated failure') });
193
-
194
- expect(demux.active).toBe(false);
195
-
196
- // This starts a new source
197
- const delayed3 = delayEach(10)(demux.subscribe('u1', controller.signal));
198
- const results3 = await toArray(take(6)(delayed3));
199
- expect(results3.length).toEqual(6);
200
- expect(results3[0]).toEqual(-1); // Initial value
201
- // There should be approximately 10ms between each value, but we allow for some slack
202
- expect(results3[5]).toBeGreaterThan(2005);
203
- expect(results3[5]).toBeLessThan(2200);
204
- });
205
- });