@powersync/common 1.50.0 → 1.52.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 (45) hide show
  1. package/dist/bundle.cjs +558 -481
  2. package/dist/bundle.cjs.map +1 -1
  3. package/dist/bundle.mjs +558 -480
  4. package/dist/bundle.mjs.map +1 -1
  5. package/dist/bundle.node.cjs +556 -481
  6. package/dist/bundle.node.cjs.map +1 -1
  7. package/dist/bundle.node.mjs +556 -480
  8. package/dist/bundle.node.mjs.map +1 -1
  9. package/dist/index.d.cts +73 -73
  10. package/lib/client/AbstractPowerSyncDatabase.js +3 -3
  11. package/lib/client/AbstractPowerSyncDatabase.js.map +1 -1
  12. package/lib/client/sync/stream/AbstractRemote.d.ts +29 -8
  13. package/lib/client/sync/stream/AbstractRemote.js +154 -177
  14. package/lib/client/sync/stream/AbstractRemote.js.map +1 -1
  15. package/lib/client/sync/stream/AbstractStreamingSyncImplementation.d.ts +1 -0
  16. package/lib/client/sync/stream/AbstractStreamingSyncImplementation.js +69 -88
  17. package/lib/client/sync/stream/AbstractStreamingSyncImplementation.js.map +1 -1
  18. package/lib/index.d.ts +1 -1
  19. package/lib/index.js +0 -1
  20. package/lib/index.js.map +1 -1
  21. package/lib/utils/async.d.ts +0 -9
  22. package/lib/utils/async.js +0 -9
  23. package/lib/utils/async.js.map +1 -1
  24. package/lib/utils/mutex.d.ts +32 -3
  25. package/lib/utils/mutex.js +85 -36
  26. package/lib/utils/mutex.js.map +1 -1
  27. package/lib/utils/queue.d.ts +16 -0
  28. package/lib/utils/queue.js +42 -0
  29. package/lib/utils/queue.js.map +1 -0
  30. package/lib/utils/stream_transform.d.ts +39 -0
  31. package/lib/utils/stream_transform.js +206 -0
  32. package/lib/utils/stream_transform.js.map +1 -0
  33. package/package.json +9 -7
  34. package/src/client/AbstractPowerSyncDatabase.ts +3 -3
  35. package/src/client/sync/stream/AbstractRemote.ts +182 -206
  36. package/src/client/sync/stream/AbstractStreamingSyncImplementation.ts +82 -83
  37. package/src/index.ts +1 -1
  38. package/src/utils/async.ts +0 -11
  39. package/src/utils/mutex.ts +111 -48
  40. package/src/utils/queue.ts +48 -0
  41. package/src/utils/stream_transform.ts +252 -0
  42. package/lib/utils/DataStream.d.ts +0 -62
  43. package/lib/utils/DataStream.js +0 -169
  44. package/lib/utils/DataStream.js.map +0 -1
  45. package/src/utils/DataStream.ts +0 -222
@@ -4,7 +4,6 @@ import { InternalProgressInformation } from '../../../db/crud/SyncProgress.js';
4
4
  import { SyncStatus, SyncStatusOptions } from '../../../db/crud/SyncStatus.js';
5
5
  import { AbortOperation } from '../../../utils/AbortOperation.js';
6
6
  import { BaseListener, BaseObserver, BaseObserverInterface, Disposable } from '../../../utils/BaseObserver.js';
7
- import { DataStream } from '../../../utils/DataStream.js';
8
7
  import { throttleLeadingTrailing } from '../../../utils/async.js';
9
8
  import {
10
9
  BucketChecksum,
@@ -19,7 +18,6 @@ import { AbstractRemote, FetchStrategy, SyncStreamOptions } from './AbstractRemo
19
18
  import { EstablishSyncStream, Instruction, coreStatusToJs } from './core-instruction.js';
20
19
  import {
21
20
  BucketRequest,
22
- CrudUploadNotification,
23
21
  StreamingSyncLine,
24
22
  StreamingSyncLineOrCrudUploadComplete,
25
23
  StreamingSyncRequestParameterType,
@@ -30,6 +28,15 @@ import {
30
28
  isStreamingSyncCheckpointPartiallyComplete,
31
29
  isStreamingSyncData
32
30
  } from './streaming-sync-types.js';
31
+ import {
32
+ extractBsonObjects,
33
+ extractJsonLines,
34
+ injectable,
35
+ InjectableIterator,
36
+ map,
37
+ SimpleAsyncIterator
38
+ } from '../../../utils/stream_transform.js';
39
+ import type { BSON } from 'bson';
33
40
 
34
41
  export enum LockType {
35
42
  CRUD = 'crud',
@@ -682,6 +689,27 @@ The next upload iteration will be delayed.`);
682
689
  });
683
690
  }
684
691
 
692
+ private async receiveSyncLines(data: {
693
+ options: SyncStreamOptions;
694
+ connection: RequiredPowerSyncConnectionOptions;
695
+ bson?: typeof BSON;
696
+ }): Promise<SimpleAsyncIterator<Uint8Array | string>> {
697
+ const { options, connection, bson } = data;
698
+ const remote = this.options.remote;
699
+
700
+ if (connection.connectionMethod == SyncStreamConnectionMethod.HTTP) {
701
+ return await remote.fetchStream(options);
702
+ } else {
703
+ return await this.options.remote.socketStreamRaw(
704
+ {
705
+ ...options,
706
+ ...{ fetchStrategy: connection.fetchStrategy }
707
+ },
708
+ bson
709
+ );
710
+ }
711
+ }
712
+
685
713
  private async legacyStreamingSyncIteration(signal: AbortSignal, resolvedOptions: RequiredPowerSyncConnectionOptions) {
686
714
  const rawTables = resolvedOptions.serializedSchema?.raw_tables;
687
715
  if (rawTables != null && rawTables.length) {
@@ -717,46 +745,31 @@ The next upload iteration will be delayed.`);
717
745
  }
718
746
  };
719
747
 
720
- let stream: DataStream<StreamingSyncLineOrCrudUploadComplete>;
721
- if (resolvedOptions?.connectionMethod == SyncStreamConnectionMethod.HTTP) {
722
- stream = await this.options.remote.postStreamRaw(syncOptions, (line: string | CrudUploadNotification) => {
748
+ const bson = await this.options.remote.getBSON();
749
+ const source = await this.receiveSyncLines({
750
+ options: syncOptions,
751
+ connection: resolvedOptions,
752
+ bson
753
+ });
754
+ const stream: InjectableIterator<StreamingSyncLineOrCrudUploadComplete> = injectable(
755
+ map(source, (line) => {
723
756
  if (typeof line == 'string') {
724
757
  return JSON.parse(line) as StreamingSyncLine;
725
758
  } else {
726
- // Directly enqueued by us
727
- return line;
759
+ return bson.deserialize(line) as StreamingSyncLine;
728
760
  }
729
- });
730
- } else {
731
- const bson = await this.options.remote.getBSON();
732
- stream = await this.options.remote.socketStreamRaw(
733
- {
734
- ...syncOptions,
735
- ...{ fetchStrategy: resolvedOptions.fetchStrategy }
736
- },
737
- (payload: Uint8Array | CrudUploadNotification) => {
738
- if (payload instanceof Uint8Array) {
739
- return bson.deserialize(payload) as StreamingSyncLine;
740
- } else {
741
- // Directly enqueued by us
742
- return payload;
743
- }
744
- },
745
- bson
746
- );
747
- }
761
+ })
762
+ );
748
763
 
749
764
  this.logger.debug('Stream established. Processing events');
750
765
 
751
766
  this.notifyCompletedUploads = () => {
752
- if (!stream.closed) {
753
- stream.enqueueData({ crud_upload_completed: null });
754
- }
767
+ stream.inject({ crud_upload_completed: null });
755
768
  };
756
769
 
757
- while (!stream.closed) {
758
- const line = await stream.read();
759
- if (!line) {
770
+ while (true) {
771
+ const { value: line, done } = await stream.next();
772
+ if (done) {
760
773
  // The stream has closed while waiting
761
774
  return;
762
775
  }
@@ -942,6 +955,11 @@ The next upload iteration will be delayed.`);
942
955
  const syncImplementation = this;
943
956
  const adapter = this.options.adapter;
944
957
  const remote = this.options.remote;
958
+ const controller = new AbortController();
959
+ const abort = () => {
960
+ return controller.abort(signal.reason);
961
+ };
962
+ signal.addEventListener('abort', abort);
945
963
  let receivingLines: Promise<void> | null = null;
946
964
  let hadSyncLine = false;
947
965
  let hideDisconnectOnRestart = false;
@@ -949,64 +967,53 @@ The next upload iteration will be delayed.`);
949
967
  if (signal.aborted) {
950
968
  throw new AbortOperation('Connection request has been aborted');
951
969
  }
952
- const abortController = new AbortController();
953
- signal.addEventListener('abort', () => abortController.abort());
954
970
 
955
971
  // Pending sync lines received from the service, as well as local events that trigger a powersync_control
956
972
  // invocation (local events include refreshed tokens and completed uploads).
957
973
  // This is a single data stream so that we can handle all control calls from a single place.
958
- let controlInvocations: DataStream<EnqueuedCommand, Uint8Array | EnqueuedCommand> | null = null;
974
+ let controlInvocations: InjectableIterator<EnqueuedCommand> | null = null;
959
975
 
960
976
  async function connect(instr: EstablishSyncStream) {
961
977
  const syncOptions: SyncStreamOptions = {
962
978
  path: '/sync/stream',
963
- abortSignal: abortController.signal,
979
+ abortSignal: controller.signal,
964
980
  data: instr.request
965
981
  };
966
982
 
967
- if (resolvedOptions.connectionMethod == SyncStreamConnectionMethod.HTTP) {
968
- controlInvocations = await remote.postStreamRaw(syncOptions, (line: string | EnqueuedCommand) => {
969
- if (typeof line == 'string') {
970
- return {
971
- command: PowerSyncControlCommand.PROCESS_TEXT_LINE,
972
- payload: line
973
- };
974
- } else {
975
- // Directly enqueued by us
976
- return line;
977
- }
978
- });
979
- } else {
980
- controlInvocations = await remote.socketStreamRaw(
981
- {
982
- ...syncOptions,
983
- fetchStrategy: resolvedOptions.fetchStrategy
984
- },
985
- (payload: Uint8Array | EnqueuedCommand) => {
986
- if (payload instanceof Uint8Array) {
983
+ controlInvocations = injectable(
984
+ map(
985
+ await syncImplementation.receiveSyncLines({
986
+ options: syncOptions,
987
+ connection: resolvedOptions
988
+ }),
989
+ (line) => {
990
+ if (typeof line == 'string') {
987
991
  return {
988
- command: PowerSyncControlCommand.PROCESS_BSON_LINE,
989
- payload: payload
992
+ command: PowerSyncControlCommand.PROCESS_TEXT_LINE,
993
+ payload: line
990
994
  };
991
995
  } else {
992
- // Directly enqueued by us
993
- return payload;
996
+ return {
997
+ command: PowerSyncControlCommand.PROCESS_BSON_LINE,
998
+ payload: line
999
+ };
994
1000
  }
995
1001
  }
996
- );
997
- }
1002
+ )
1003
+ );
998
1004
 
999
1005
  // The rust client will set connected: true after the first sync line because that's when it gets invoked, but
1000
1006
  // we're already connected here and can report that.
1001
1007
  syncImplementation.updateSyncStatus({ connected: true });
1002
1008
 
1003
1009
  try {
1004
- while (!controlInvocations.closed) {
1005
- const line = await controlInvocations.read();
1006
- if (line == null) {
1007
- return;
1010
+ while (true) {
1011
+ let event = await controlInvocations.next();
1012
+ if (event.done) {
1013
+ break;
1008
1014
  }
1009
1015
 
1016
+ const line = event.value;
1010
1017
  await control(line.command, line.payload);
1011
1018
 
1012
1019
  if (!hadSyncLine) {
@@ -1015,12 +1022,8 @@ The next upload iteration will be delayed.`);
1015
1022
  }
1016
1023
  }
1017
1024
  } finally {
1018
- const activeInstructions = controlInvocations;
1019
- // We concurrently add events to the active data stream when e.g. a CRUD upload is completed or a token is
1020
- // refreshed. That would throw after closing (and we can't handle those events either way), so set this back
1021
- // to null.
1022
- controlInvocations = null;
1023
- await activeInstructions.close();
1025
+ abort();
1026
+ signal.removeEventListener('abort', abort);
1024
1027
  }
1025
1028
  }
1026
1029
 
@@ -1072,7 +1075,7 @@ The next upload iteration will be delayed.`);
1072
1075
  // Restart iteration after the credentials have been refreshed.
1073
1076
  remote.fetchCredentials().then(
1074
1077
  (_) => {
1075
- controlInvocations?.enqueueData({ command: PowerSyncControlCommand.NOTIFY_TOKEN_REFRESHED });
1078
+ controlInvocations?.inject({ command: PowerSyncControlCommand.NOTIFY_TOKEN_REFRESHED });
1076
1079
  },
1077
1080
  (err) => {
1078
1081
  syncImplementation.logger.warn('Could not prefetch credentials', err);
@@ -1080,7 +1083,7 @@ The next upload iteration will be delayed.`);
1080
1083
  );
1081
1084
  }
1082
1085
  } else if ('CloseSyncStream' in instruction) {
1083
- abortController.abort();
1086
+ controller.abort();
1084
1087
  hideDisconnectOnRestart = instruction.CloseSyncStream.hide_disconnect;
1085
1088
  } else if ('FlushFileSystem' in instruction) {
1086
1089
  // Not necessary on JS platforms.
@@ -1113,17 +1116,13 @@ The next upload iteration will be delayed.`);
1113
1116
  await control(PowerSyncControlCommand.START, JSON.stringify(options));
1114
1117
 
1115
1118
  this.notifyCompletedUploads = () => {
1116
- if (controlInvocations && !controlInvocations?.closed) {
1117
- controlInvocations.enqueueData({ command: PowerSyncControlCommand.NOTIFY_CRUD_UPLOAD_COMPLETED });
1118
- }
1119
+ controlInvocations?.inject({ command: PowerSyncControlCommand.NOTIFY_CRUD_UPLOAD_COMPLETED });
1119
1120
  };
1120
1121
  this.handleActiveStreamsChange = () => {
1121
- if (controlInvocations && !controlInvocations?.closed) {
1122
- controlInvocations.enqueueData({
1123
- command: PowerSyncControlCommand.UPDATE_SUBSCRIPTIONS,
1124
- payload: JSON.stringify(this.activeStreams)
1125
- });
1126
- }
1122
+ controlInvocations?.inject({
1123
+ command: PowerSyncControlCommand.UPDATE_SUBSCRIPTIONS,
1124
+ payload: JSON.stringify(this.activeStreams)
1125
+ });
1127
1126
  };
1128
1127
  await receivingLines;
1129
1128
  } finally {
package/src/index.ts CHANGED
@@ -59,9 +59,9 @@ export * from './client/watched/WatchedQuery.js';
59
59
  export * from './utils/AbortOperation.js';
60
60
  export * from './utils/BaseObserver.js';
61
61
  export * from './utils/ControlledExecutor.js';
62
- export * from './utils/DataStream.js';
63
62
  export * from './utils/Logger.js';
64
63
  export * from './utils/mutex.js';
65
64
  export * from './utils/parseQuery.js';
65
+ export type { SimpleAsyncIterator } from './utils/stream_transform.js';
66
66
 
67
67
  export * from './types/types.js';
@@ -1,14 +1,3 @@
1
- /**
2
- * A ponyfill for `Symbol.asyncIterator` that is compatible with the
3
- * [recommended polyfill](https://github.com/Azure/azure-sdk-for-js/blob/%40azure/core-asynciterator-polyfill_1.0.2/sdk/core/core-asynciterator-polyfill/src/index.ts#L4-L6)
4
- * we recommend for React Native.
5
- *
6
- * As long as we use this symbol (instead of `for await` and `async *`) in this package, we can be compatible with async
7
- * iterators without requiring them.
8
- */
9
- export const symbolAsyncIterator: typeof Symbol.asyncIterator =
10
- Symbol.asyncIterator ?? Symbol.for('Symbol.asyncIterator');
11
-
12
1
  /**
13
2
  * Throttle a function to be called at most once every "wait" milliseconds,
14
3
  * on the trailing edge.
@@ -1,21 +1,32 @@
1
+ import { Queue } from './queue.js';
2
+
1
3
  export type UnlockFn = () => void;
2
4
 
3
5
  /**
4
- * An asynchronous mutex implementation.
6
+ * An asynchronous semaphore implementation with associated items per lease.
5
7
  *
6
8
  * @internal This class is meant to be used in PowerSync SDKs only, and is not part of the public API.
7
9
  */
8
- export class Mutex {
9
- private inCriticalSection = false;
10
+ export class Semaphore<T> {
11
+ // Available items that are not currently assigned to a waiter.
12
+ private readonly available: Queue<T>;
10
13
 
14
+ readonly size: number;
11
15
  // Linked list of waiters. We don't expect the wait list to become particularly large, and this allows removing
12
16
  // aborted waiters from the middle of the list efficiently.
13
- private firstWaiter?: MutexWaitNode;
14
- private lastWaiter?: MutexWaitNode;
17
+ private firstWaiter?: SemaphoreWaitNode<T>;
18
+ private lastWaiter?: SemaphoreWaitNode<T>;
15
19
 
16
- private addWaiter(onAcquire: () => void): MutexWaitNode {
17
- const node: MutexWaitNode = {
20
+ constructor(elements: Iterable<T>) {
21
+ this.available = new Queue(elements);
22
+ this.size = this.available.length;
23
+ }
24
+
25
+ private addWaiter(requestedItems: number, onAcquire: () => void): SemaphoreWaitNode<T> {
26
+ const node: SemaphoreWaitNode<T> = {
18
27
  isActive: true,
28
+ acquiredItems: [],
29
+ remainingItems: requestedItems,
19
30
  onAcquire,
20
31
  prev: this.lastWaiter
21
32
  };
@@ -30,7 +41,7 @@ export class Mutex {
30
41
  return node;
31
42
  }
32
43
 
33
- private deactivateWaiter(waiter: MutexWaitNode) {
44
+ private deactivateWaiter(waiter: SemaphoreWaitNode<T>) {
34
45
  const { prev, next } = waiter;
35
46
  waiter.isActive = false;
36
47
 
@@ -40,77 +51,129 @@ export class Mutex {
40
51
  if (waiter == this.lastWaiter) this.lastWaiter = prev;
41
52
  }
42
53
 
43
- acquire(abort?: AbortSignal): Promise<UnlockFn> {
54
+ private requestPermits(amount: number, abort?: AbortSignal): Promise<{ items: T[]; release: UnlockFn }> {
55
+ if (amount <= 0 || amount > this.size) {
56
+ throw new Error(`Invalid amount of items requested (${amount}), must be between 1 and ${this.size}`);
57
+ }
58
+
44
59
  return new Promise((resolve, reject) => {
45
60
  function rejectAborted() {
46
- reject(abort?.reason ?? new Error('Mutex acquire aborted'));
61
+ reject(abort?.reason ?? new Error('Semaphore acquire aborted'));
47
62
  }
48
63
  if (abort?.aborted) {
49
64
  return rejectAborted();
50
65
  }
51
66
 
52
- let holdsMutex = false;
67
+ let waiter: SemaphoreWaitNode<T>;
53
68
 
54
69
  const markCompleted = () => {
55
- if (!holdsMutex) return;
56
- holdsMutex = false;
70
+ const items = waiter.acquiredItems;
71
+ waiter.acquiredItems = []; // Avoid releasing items twice.
72
+
73
+ for (const element of items) {
74
+ // Give to next waiter, if possible.
75
+ const nextWaiter = this.firstWaiter;
76
+ if (nextWaiter) {
77
+ nextWaiter.acquiredItems.push(element);
78
+ nextWaiter.remainingItems--;
79
+ if (nextWaiter.remainingItems == 0) {
80
+ nextWaiter.onAcquire();
81
+ }
82
+ } else {
83
+ // No pending waiter, return lease into pool.
84
+ this.available.addLast(element);
85
+ }
86
+ }
87
+ };
88
+
89
+ const onAbort = () => {
90
+ abort?.removeEventListener('abort', onAbort);
57
91
 
58
- const waiter = this.firstWaiter;
59
- if (waiter) {
92
+ if (waiter.isActive) {
60
93
  this.deactivateWaiter(waiter);
61
- // Still in critical section, but owned by next waiter now.
62
- waiter.onAcquire();
63
- } else {
64
- this.inCriticalSection = false;
94
+ rejectAborted();
65
95
  }
66
96
  };
67
97
 
68
- if (!this.inCriticalSection) {
69
- this.inCriticalSection = true;
70
- holdsMutex = true;
71
- return resolve(markCompleted);
72
- } else {
73
- let node: MutexWaitNode;
98
+ const resolvePromise = () => {
99
+ this.deactivateWaiter(waiter);
100
+ abort?.removeEventListener('abort', onAbort);
74
101
 
75
- const onAbort = () => {
76
- abort?.removeEventListener('abort', onAbort);
102
+ const items = waiter.acquiredItems;
103
+ resolve({ items, release: markCompleted });
104
+ };
77
105
 
78
- if (node.isActive) {
79
- this.deactivateWaiter(node);
80
- rejectAborted();
81
- }
82
- };
106
+ waiter = this.addWaiter(amount, resolvePromise);
83
107
 
84
- node = this.addWaiter(() => {
85
- abort?.removeEventListener('abort', onAbort);
86
- holdsMutex = true;
87
- resolve(markCompleted);
88
- });
108
+ // If there are items in the pool that haven't been assigned, we can pull them into this waiter. Note that this is
109
+ // only the case if we're the first waiter (otherwise, items would have been assigned to an earlier waiter).
110
+ while (!this.available.isEmpty && waiter.remainingItems > 0) {
111
+ waiter.acquiredItems.push(this.available.removeFirst());
112
+ waiter.remainingItems--;
113
+ }
89
114
 
90
- abort?.addEventListener('abort', onAbort);
115
+ if (waiter.remainingItems == 0) {
116
+ return resolvePromise();
91
117
  }
118
+
119
+ abort?.addEventListener('abort', onAbort);
92
120
  });
93
121
  }
94
122
 
95
- async runExclusive<T>(fn: () => PromiseLike<T> | T, abort?: AbortSignal): Promise<T> {
96
- const returnMutex = await this.acquire(abort);
123
+ /**
124
+ * Requests a single item from the pool.
125
+ *
126
+ * The returned `release` callback must be invoked to return the item into the pool.
127
+ */
128
+ async requestOne(abort?: AbortSignal): Promise<{ item: T; release: UnlockFn }> {
129
+ const { items, release } = await this.requestPermits(1, abort);
130
+ return { release, item: items[0] };
131
+ }
97
132
 
98
- try {
99
- return await fn();
100
- } finally {
101
- returnMutex();
102
- }
133
+ /**
134
+ * Requests access to all items from the pool.
135
+ *
136
+ * The returned `release` callback must be invoked to return items into the pool.
137
+ */
138
+ requestAll(abort?: AbortSignal): Promise<{ items: T[]; release: UnlockFn }> {
139
+ return this.requestPermits(this.size, abort);
103
140
  }
104
141
  }
105
142
 
106
- interface MutexWaitNode {
143
+ interface SemaphoreWaitNode<T> {
107
144
  /**
108
145
  * Whether the waiter is currently active (not aborted and not fullfilled).
109
146
  */
110
147
  isActive: boolean;
148
+ acquiredItems: T[];
149
+ remainingItems: number;
111
150
  onAcquire: () => void;
112
- prev?: MutexWaitNode;
113
- next?: MutexWaitNode;
151
+ prev?: SemaphoreWaitNode<T>;
152
+ next?: SemaphoreWaitNode<T>;
153
+ }
154
+
155
+ /**
156
+ * An asynchronous mutex implementation.
157
+ *
158
+ * @internal This class is meant to be used in PowerSync SDKs only, and is not part of the public API.
159
+ */
160
+ export class Mutex {
161
+ private inner = new Semaphore([null]);
162
+
163
+ async acquire(abort?: AbortSignal): Promise<UnlockFn> {
164
+ const { release } = await this.inner.requestOne(abort);
165
+ return release;
166
+ }
167
+
168
+ async runExclusive<T>(fn: () => PromiseLike<T> | T, abort?: AbortSignal): Promise<T> {
169
+ const returnMutex = await this.acquire(abort);
170
+
171
+ try {
172
+ return await fn();
173
+ } finally {
174
+ returnMutex();
175
+ }
176
+ }
114
177
  }
115
178
 
116
179
  /**
@@ -0,0 +1,48 @@
1
+ /**
2
+ * A simple fixed-capacity queue implementation.
3
+ *
4
+ * Unlike a naive queue implemented by `array.push()` and `array.shift()`, this avoids moving array elements around
5
+ * and is `O(1)` for {@link addLast} and {@link removeFirst}.
6
+ */
7
+ export class Queue<T> {
8
+ private table: (T | undefined)[];
9
+ // Index of the first element in the table.
10
+ private head: number;
11
+ // Amount of items currently in the queue.
12
+ private _length: number;
13
+
14
+ constructor(initialItems: Iterable<T>) {
15
+ this.table = [...initialItems];
16
+ this.head = 0;
17
+ this._length = this.table.length;
18
+ }
19
+
20
+ get isEmpty(): boolean {
21
+ return this.length == 0;
22
+ }
23
+
24
+ get length(): number {
25
+ return this._length;
26
+ }
27
+
28
+ removeFirst(): T {
29
+ if (this.isEmpty) {
30
+ throw new Error('Queue is empty');
31
+ }
32
+
33
+ const result = this.table[this.head] as T;
34
+ this._length--;
35
+ this.table[this.head] = undefined;
36
+ this.head = (this.head + 1) % this.table.length;
37
+ return result;
38
+ }
39
+
40
+ addLast(element: T) {
41
+ if (this.length == this.table.length) {
42
+ throw new Error('Queue is full');
43
+ }
44
+
45
+ this.table[(this.head + this._length) % this.table.length] = element;
46
+ this._length++;
47
+ }
48
+ }