@powersync/common 1.23.0 → 1.25.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.
@@ -4,7 +4,8 @@ import { AbortOperation } from '../../../utils/AbortOperation.js';
4
4
  import { BaseObserver } from '../../../utils/BaseObserver.js';
5
5
  import { throttleLeadingTrailing } from '../../../utils/throttle.js';
6
6
  import { SyncDataBucket } from '../bucket/SyncDataBucket.js';
7
- import { isStreamingKeepalive, isStreamingSyncCheckpoint, isStreamingSyncCheckpointComplete, isStreamingSyncCheckpointDiff, isStreamingSyncData } from './streaming-sync-types.js';
7
+ import { FetchStrategy } from './AbstractRemote.js';
8
+ import { isStreamingKeepalive, isStreamingSyncCheckpoint, isStreamingSyncCheckpointComplete, isStreamingSyncCheckpointDiff, isStreamingSyncCheckpointPartiallyComplete, isStreamingSyncData } from './streaming-sync-types.js';
8
9
  export var LockType;
9
10
  (function (LockType) {
10
11
  LockType["CRUD"] = "crud";
@@ -24,8 +25,14 @@ export const DEFAULT_STREAMING_SYNC_OPTIONS = {
24
25
  };
25
26
  export const DEFAULT_STREAM_CONNECTION_OPTIONS = {
26
27
  connectionMethod: SyncStreamConnectionMethod.WEB_SOCKET,
28
+ fetchStrategy: FetchStrategy.Buffered,
27
29
  params: {}
28
30
  };
31
+ // The priority we assume when we receive checkpoint lines where no priority is set.
32
+ // This is the default priority used by the sync service, but can be set to an arbitrary
33
+ // value since sync services without priorities also won't send partial sync completion
34
+ // messages.
35
+ const FALLBACK_PRIORITY = 3;
29
36
  export class AbstractStreamingSyncImplementation extends BaseObserver {
30
37
  _lastSyncedAt;
31
38
  options;
@@ -39,6 +46,7 @@ export class AbstractStreamingSyncImplementation extends BaseObserver {
39
46
  this.options = { ...DEFAULT_STREAMING_SYNC_OPTIONS, ...options };
40
47
  this.syncStatus = new SyncStatus({
41
48
  connected: false,
49
+ connecting: false,
42
50
  lastSyncedAt: undefined,
43
51
  dataFlow: {
44
52
  uploading: false,
@@ -55,23 +63,32 @@ export class AbstractStreamingSyncImplementation extends BaseObserver {
55
63
  }
56
64
  async waitForReady() { }
57
65
  waitForStatus(status) {
66
+ return this.waitUntilStatusMatches((currentStatus) => {
67
+ /**
68
+ * Match only the partial status options provided in the
69
+ * matching status
70
+ */
71
+ const matchPartialObject = (compA, compB) => {
72
+ return Object.entries(compA).every(([key, value]) => {
73
+ const comparisonBValue = compB[key];
74
+ if (typeof value == 'object' && typeof comparisonBValue == 'object') {
75
+ return matchPartialObject(value, comparisonBValue);
76
+ }
77
+ return value == comparisonBValue;
78
+ });
79
+ };
80
+ return matchPartialObject(status, currentStatus);
81
+ });
82
+ }
83
+ waitUntilStatusMatches(predicate) {
58
84
  return new Promise((resolve) => {
85
+ if (predicate(this.syncStatus)) {
86
+ resolve();
87
+ return;
88
+ }
59
89
  const l = this.registerListener({
60
90
  statusChanged: (updatedStatus) => {
61
- /**
62
- * Match only the partial status options provided in the
63
- * matching status
64
- */
65
- const matchPartialObject = (compA, compB) => {
66
- return Object.entries(compA).every(([key, value]) => {
67
- const comparisonBValue = compB[key];
68
- if (typeof value == 'object' && typeof comparisonBValue == 'object') {
69
- return matchPartialObject(value, comparisonBValue);
70
- }
71
- return value == comparisonBValue;
72
- });
73
- };
74
- if (matchPartialObject(status, updatedStatus.toJSON())) {
91
+ if (predicate(updatedStatus)) {
75
92
  resolve();
76
93
  l?.();
77
94
  }
@@ -208,7 +225,7 @@ The next upload iteration will be delayed.`);
208
225
  }
209
226
  this.streamingSyncPromise = undefined;
210
227
  this.abortController = null;
211
- this.updateSyncStatus({ connected: false });
228
+ this.updateSyncStatus({ connected: false, connecting: false });
212
229
  }
213
230
  /**
214
231
  * @deprecated use [connect instead]
@@ -239,6 +256,7 @@ The next upload iteration will be delayed.`);
239
256
  this.crudUpdateListener = undefined;
240
257
  this.updateSyncStatus({
241
258
  connected: false,
259
+ connecting: false,
242
260
  dataFlow: {
243
261
  downloading: false
244
262
  }
@@ -251,6 +269,7 @@ The next upload iteration will be delayed.`);
251
269
  * - Close any sync stream ReadableStreams (which will also close any established network requests)
252
270
  */
253
271
  while (true) {
272
+ this.updateSyncStatus({ connecting: true });
254
273
  try {
255
274
  if (signal?.aborted) {
256
275
  break;
@@ -281,6 +300,7 @@ The next upload iteration will be delayed.`);
281
300
  else {
282
301
  this.logger.error(ex);
283
302
  }
303
+ // On error, wait a little before retrying
284
304
  await this.delayRetry();
285
305
  }
286
306
  finally {
@@ -289,13 +309,25 @@ The next upload iteration will be delayed.`);
289
309
  nestedAbortController = new AbortController();
290
310
  }
291
311
  this.updateSyncStatus({
292
- connected: false
312
+ connected: false,
313
+ connecting: true // May be unnecessary
293
314
  });
294
- // On error, wait a little before retrying
295
315
  }
296
316
  }
297
317
  // Mark as disconnected if here
298
- this.updateSyncStatus({ connected: false });
318
+ this.updateSyncStatus({ connected: false, connecting: false });
319
+ }
320
+ async collectLocalBucketState() {
321
+ const bucketEntries = await this.options.adapter.getBucketStates();
322
+ const req = bucketEntries.map((entry) => ({
323
+ name: entry.bucket,
324
+ after: entry.op_id
325
+ }));
326
+ const localDescriptions = new Map();
327
+ for (const entry of bucketEntries) {
328
+ localDescriptions.set(entry.bucket, null);
329
+ }
330
+ return [req, localDescriptions];
299
331
  }
300
332
  async streamingSyncIteration(signal, options) {
301
333
  return await this.obtainLock({
@@ -308,20 +340,11 @@ The next upload iteration will be delayed.`);
308
340
  };
309
341
  this.logger.debug('Streaming sync iteration started');
310
342
  this.options.adapter.startSession();
311
- const bucketEntries = await this.options.adapter.getBucketStates();
312
- const initialBuckets = new Map();
313
- bucketEntries.forEach((entry) => {
314
- initialBuckets.set(entry.bucket, entry.op_id);
315
- });
316
- const req = Array.from(initialBuckets.entries()).map(([bucket, after]) => ({
317
- name: bucket,
318
- after: after
319
- }));
343
+ let [req, bucketMap] = await this.collectLocalBucketState();
320
344
  // These are compared by reference
321
345
  let targetCheckpoint = null;
322
346
  let validatedCheckpoint = null;
323
347
  let appliedCheckpoint = null;
324
- let bucketSet = new Set(initialBuckets.keys());
325
348
  const clientId = await this.options.adapter.getClientId();
326
349
  this.logger.debug('Requesting stream from server');
327
350
  const syncOptions = {
@@ -335,9 +358,16 @@ The next upload iteration will be delayed.`);
335
358
  client_id: clientId
336
359
  }
337
360
  };
338
- const stream = resolvedOptions?.connectionMethod == SyncStreamConnectionMethod.HTTP
339
- ? await this.options.remote.postStream(syncOptions)
340
- : await this.options.remote.socketStream(syncOptions);
361
+ let stream;
362
+ if (resolvedOptions?.connectionMethod == SyncStreamConnectionMethod.HTTP) {
363
+ stream = await this.options.remote.postStream(syncOptions);
364
+ }
365
+ else {
366
+ stream = await this.options.remote.socketStream({
367
+ ...syncOptions,
368
+ ...{ fetchStrategy: resolvedOptions.fetchStrategy }
369
+ });
370
+ }
341
371
  this.logger.debug('Stream established. Processing events');
342
372
  while (!stream.closed) {
343
373
  const line = await stream.read();
@@ -355,16 +385,19 @@ The next upload iteration will be delayed.`);
355
385
  }
356
386
  if (isStreamingSyncCheckpoint(line)) {
357
387
  targetCheckpoint = line.checkpoint;
358
- const bucketsToDelete = new Set(bucketSet);
359
- const newBuckets = new Set();
388
+ const bucketsToDelete = new Set(bucketMap.keys());
389
+ const newBuckets = new Map();
360
390
  for (const checksum of line.checkpoint.buckets) {
361
- newBuckets.add(checksum.bucket);
391
+ newBuckets.set(checksum.bucket, {
392
+ name: checksum.bucket,
393
+ priority: checksum.priority ?? FALLBACK_PRIORITY
394
+ });
362
395
  bucketsToDelete.delete(checksum.bucket);
363
396
  }
364
397
  if (bucketsToDelete.size > 0) {
365
398
  this.logger.debug('Removing buckets', [...bucketsToDelete]);
366
399
  }
367
- bucketSet = newBuckets;
400
+ bucketMap = newBuckets;
368
401
  await this.options.adapter.removeBuckets([...bucketsToDelete]);
369
402
  await this.options.adapter.setTargetCheckpoint(targetCheckpoint);
370
403
  }
@@ -395,6 +428,35 @@ The next upload iteration will be delayed.`);
395
428
  }
396
429
  validatedCheckpoint = targetCheckpoint;
397
430
  }
431
+ else if (isStreamingSyncCheckpointPartiallyComplete(line)) {
432
+ const priority = line.partial_checkpoint_complete.priority;
433
+ this.logger.debug('Partial checkpoint complete', priority);
434
+ const result = await this.options.adapter.syncLocalDatabase(targetCheckpoint, priority);
435
+ if (!result.checkpointValid) {
436
+ // This means checksums failed. Start again with a new checkpoint.
437
+ // TODO: better back-off
438
+ await new Promise((resolve) => setTimeout(resolve, 50));
439
+ return { retry: true };
440
+ }
441
+ else if (!result.ready) {
442
+ // Need more data for a consistent partial sync within a priority - continue waiting.
443
+ }
444
+ else {
445
+ // We'll keep on downloading, but can report that this priority is synced now.
446
+ this.logger.debug('partial checkpoint validation succeeded');
447
+ // All states with a higher priority can be deleted since this partial sync includes them.
448
+ const priorityStates = this.syncStatus.priorityStatusEntries.filter((s) => s.priority <= priority);
449
+ priorityStates.push({
450
+ priority,
451
+ lastSyncedAt: new Date(),
452
+ hasSynced: true
453
+ });
454
+ this.updateSyncStatus({
455
+ connected: true,
456
+ priorityStatusEntries: priorityStates
457
+ });
458
+ }
459
+ }
398
460
  else if (isStreamingSyncCheckpointDiff(line)) {
399
461
  // TODO: It may be faster to just keep track of the diff, instead of the entire checkpoint
400
462
  if (targetCheckpoint == null) {
@@ -417,7 +479,11 @@ The next upload iteration will be delayed.`);
417
479
  write_checkpoint: diff.write_checkpoint
418
480
  };
419
481
  targetCheckpoint = newCheckpoint;
420
- bucketSet = new Set(newBuckets.keys());
482
+ bucketMap = new Map();
483
+ newBuckets.forEach((checksum, name) => bucketMap.set(name, {
484
+ name: checksum.bucket,
485
+ priority: checksum.priority ?? FALLBACK_PRIORITY
486
+ }));
421
487
  const bucketsToDelete = diff.removed_buckets;
422
488
  if (bucketsToDelete.length > 0) {
423
489
  this.logger.debug('Remove buckets', bucketsToDelete);
@@ -453,7 +519,8 @@ The next upload iteration will be delayed.`);
453
519
  if (targetCheckpoint === appliedCheckpoint) {
454
520
  this.updateSyncStatus({
455
521
  connected: true,
456
- lastSyncedAt: new Date()
522
+ lastSyncedAt: new Date(),
523
+ priorityStatusEntries: []
457
524
  });
458
525
  }
459
526
  else if (validatedCheckpoint === targetCheckpoint) {
@@ -473,6 +540,7 @@ The next upload iteration will be delayed.`);
473
540
  this.updateSyncStatus({
474
541
  connected: true,
475
542
  lastSyncedAt: new Date(),
543
+ priorityStatusEntries: [],
476
544
  dataFlow: {
477
545
  downloading: false
478
546
  }
@@ -490,11 +558,13 @@ The next upload iteration will be delayed.`);
490
558
  updateSyncStatus(options) {
491
559
  const updatedStatus = new SyncStatus({
492
560
  connected: options.connected ?? this.syncStatus.connected,
561
+ connecting: !options.connected && (options.connecting ?? this.syncStatus.connecting),
493
562
  lastSyncedAt: options.lastSyncedAt ?? this.syncStatus.lastSyncedAt,
494
563
  dataFlow: {
495
564
  ...this.syncStatus.dataFlowStatus,
496
565
  ...options.dataFlow
497
- }
566
+ },
567
+ priorityStatusEntries: options.priorityStatusEntries ?? this.syncStatus.priorityStatusEntries
498
568
  });
499
569
  if (!this.syncStatus.isEqual(updatedStatus)) {
500
570
  this.syncStatus = updatedStatus;
@@ -90,11 +90,17 @@ export interface StreamingSyncCheckpointComplete {
90
90
  last_op_id: OpId;
91
91
  };
92
92
  }
93
+ export interface StreamingSyncCheckpointPartiallyComplete {
94
+ partial_checkpoint_complete: {
95
+ priority: number;
96
+ last_op_id: OpId;
97
+ };
98
+ }
93
99
  export interface StreamingSyncKeepalive {
94
100
  /** If specified, token expires in this many seconds. */
95
101
  token_expires_in: number;
96
102
  }
97
- export type StreamingSyncLine = StreamingSyncDataJSON | StreamingSyncCheckpoint | StreamingSyncCheckpointDiff | StreamingSyncCheckpointComplete | StreamingSyncKeepalive;
103
+ export type StreamingSyncLine = StreamingSyncDataJSON | StreamingSyncCheckpoint | StreamingSyncCheckpointDiff | StreamingSyncCheckpointComplete | StreamingSyncCheckpointPartiallyComplete | StreamingSyncKeepalive;
98
104
  export interface BucketRequest {
99
105
  name: string;
100
106
  /**
@@ -106,6 +112,7 @@ export declare function isStreamingSyncData(line: StreamingSyncLine): line is St
106
112
  export declare function isStreamingKeepalive(line: StreamingSyncLine): line is StreamingSyncKeepalive;
107
113
  export declare function isStreamingSyncCheckpoint(line: StreamingSyncLine): line is StreamingSyncCheckpoint;
108
114
  export declare function isStreamingSyncCheckpointComplete(line: StreamingSyncLine): line is StreamingSyncCheckpointComplete;
115
+ export declare function isStreamingSyncCheckpointPartiallyComplete(line: StreamingSyncLine): line is StreamingSyncCheckpointPartiallyComplete;
109
116
  export declare function isStreamingSyncCheckpointDiff(line: StreamingSyncLine): line is StreamingSyncCheckpointDiff;
110
117
  export declare function isContinueCheckpointRequest(request: SyncRequest): request is ContinueCheckpointRequest;
111
118
  export declare function isSyncNewCheckpointRequest(request: SyncRequest): request is SyncNewCheckpointRequest;
@@ -10,6 +10,9 @@ export function isStreamingSyncCheckpoint(line) {
10
10
  export function isStreamingSyncCheckpointComplete(line) {
11
11
  return line.checkpoint_complete != null;
12
12
  }
13
+ export function isStreamingSyncCheckpointPartiallyComplete(line) {
14
+ return line.partial_checkpoint_complete != null;
15
+ }
13
16
  export function isStreamingSyncCheckpointDiff(line) {
14
17
  return line.checkpoint_diff != null;
15
18
  }
@@ -82,7 +82,7 @@ export interface DBLockOptions {
82
82
  timeoutMs?: number;
83
83
  }
84
84
  export interface DBAdapter extends BaseObserverInterface<DBAdapterListener>, DBGetUtils {
85
- close: () => void;
85
+ close: () => void | Promise<void>;
86
86
  execute: (query: string, params?: any[]) => Promise<QueryResult>;
87
87
  executeBatch: (query: string, params?: any[][]) => Promise<QueryResult>;
88
88
  name: string;
@@ -2,11 +2,18 @@ export type SyncDataFlowStatus = Partial<{
2
2
  downloading: boolean;
3
3
  uploading: boolean;
4
4
  }>;
5
+ export interface SyncPriorityStatus {
6
+ priority: number;
7
+ lastSyncedAt?: Date;
8
+ hasSynced?: boolean;
9
+ }
5
10
  export type SyncStatusOptions = {
6
11
  connected?: boolean;
12
+ connecting?: boolean;
7
13
  dataFlow?: SyncDataFlowStatus;
8
14
  lastSyncedAt?: Date;
9
15
  hasSynced?: boolean;
16
+ priorityStatusEntries?: SyncPriorityStatus[];
10
17
  };
11
18
  export declare class SyncStatus {
12
19
  protected options: SyncStatusOptions;
@@ -15,6 +22,7 @@ export declare class SyncStatus {
15
22
  * true if currently connected.
16
23
  */
17
24
  get connected(): boolean;
25
+ get connecting(): boolean;
18
26
  /**
19
27
  * Time that a last sync has fully completed, if any.
20
28
  * Currently this is reset to null after a restart.
@@ -32,7 +40,28 @@ export declare class SyncStatus {
32
40
  downloading: boolean;
33
41
  uploading: boolean;
34
42
  }>;
43
+ /**
44
+ * Partial sync status for involved bucket priorities.
45
+ */
46
+ get priorityStatusEntries(): SyncPriorityStatus[];
47
+ /**
48
+ * Reports a pair of {@link SyncStatus#hasSynced} and {@link SyncStatus#lastSyncedAt} fields that apply
49
+ * to a specific bucket priority instead of the entire sync operation.
50
+ *
51
+ * When buckets with different priorities are declared, PowerSync may choose to synchronize higher-priority
52
+ * buckets first. When a consistent view over all buckets for all priorities up until the given priority is
53
+ * reached, PowerSync makes data from those buckets available before lower-priority buckets have finished
54
+ * synchronizing.
55
+ * When PowerSync makes data for a given priority available, all buckets in higher-priorities are guaranteed to
56
+ * be consistent with that checkpoint. For this reason, this method may also return the status for lower priorities.
57
+ * In a state where the PowerSync just finished synchronizing buckets in priority level 3, calling this method
58
+ * with a priority of 1 may return information for priority level 3.
59
+ *
60
+ * @param priority The bucket priority for which the status should be reported.
61
+ */
62
+ statusForPriority(priority: number): SyncPriorityStatus;
35
63
  isEqual(status: SyncStatus): boolean;
36
64
  getMessage(): string;
37
65
  toJSON(): SyncStatusOptions;
66
+ private static comparePriorities;
38
67
  }
@@ -9,6 +9,9 @@ export class SyncStatus {
9
9
  get connected() {
10
10
  return this.options.connected ?? false;
11
11
  }
12
+ get connecting() {
13
+ return this.options.connecting ?? false;
14
+ }
12
15
  /**
13
16
  * Time that a last sync has fully completed, if any.
14
17
  * Currently this is reset to null after a restart.
@@ -39,19 +42,60 @@ export class SyncStatus {
39
42
  uploading: false
40
43
  });
41
44
  }
45
+ /**
46
+ * Partial sync status for involved bucket priorities.
47
+ */
48
+ get priorityStatusEntries() {
49
+ return (this.options.priorityStatusEntries ?? []).slice().sort(SyncStatus.comparePriorities);
50
+ }
51
+ /**
52
+ * Reports a pair of {@link SyncStatus#hasSynced} and {@link SyncStatus#lastSyncedAt} fields that apply
53
+ * to a specific bucket priority instead of the entire sync operation.
54
+ *
55
+ * When buckets with different priorities are declared, PowerSync may choose to synchronize higher-priority
56
+ * buckets first. When a consistent view over all buckets for all priorities up until the given priority is
57
+ * reached, PowerSync makes data from those buckets available before lower-priority buckets have finished
58
+ * synchronizing.
59
+ * When PowerSync makes data for a given priority available, all buckets in higher-priorities are guaranteed to
60
+ * be consistent with that checkpoint. For this reason, this method may also return the status for lower priorities.
61
+ * In a state where the PowerSync just finished synchronizing buckets in priority level 3, calling this method
62
+ * with a priority of 1 may return information for priority level 3.
63
+ *
64
+ * @param priority The bucket priority for which the status should be reported.
65
+ */
66
+ statusForPriority(priority) {
67
+ // priorityStatusEntries are sorted by ascending priorities (so higher numbers to lower numbers).
68
+ for (const known of this.priorityStatusEntries) {
69
+ // We look for the first entry that doesn't have a higher priority.
70
+ if (known.priority >= priority) {
71
+ return known;
72
+ }
73
+ }
74
+ // If we have a complete sync, that necessarily includes all priorities.
75
+ return {
76
+ priority,
77
+ lastSyncedAt: this.lastSyncedAt,
78
+ hasSynced: this.hasSynced
79
+ };
80
+ }
42
81
  isEqual(status) {
43
82
  return JSON.stringify(this.options) == JSON.stringify(status.options);
44
83
  }
45
84
  getMessage() {
46
85
  const dataFlow = this.dataFlowStatus;
47
- return `SyncStatus<connected: ${this.connected} lastSyncedAt: ${this.lastSyncedAt} hasSynced: ${this.hasSynced}. Downloading: ${dataFlow.downloading}. Uploading: ${dataFlow.uploading}`;
86
+ return `SyncStatus<connected: ${this.connected} connecting: ${this.connecting} lastSyncedAt: ${this.lastSyncedAt} hasSynced: ${this.hasSynced}. Downloading: ${dataFlow.downloading}. Uploading: ${dataFlow.uploading}`;
48
87
  }
49
88
  toJSON() {
50
89
  return {
51
90
  connected: this.connected,
91
+ connecting: this.connecting,
52
92
  dataFlow: this.dataFlowStatus,
53
93
  lastSyncedAt: this.lastSyncedAt,
54
- hasSynced: this.hasSynced
94
+ hasSynced: this.hasSynced,
95
+ priorityStatusEntries: this.priorityStatusEntries
55
96
  };
56
97
  }
98
+ static comparePriorities(a, b) {
99
+ return b.priority - a.priority; // Reverse because higher priorities have lower numbers
100
+ }
57
101
  }
@@ -63,9 +63,6 @@ export class DataStream extends BaseObserver {
63
63
  * @returns a Data payload or Null if the stream closed.
64
64
  */
65
65
  async read() {
66
- if (this.dataQueue.length <= this.lowWatermark) {
67
- await this.iterateAsyncErrored(async (l) => l.lowWater?.());
68
- }
69
66
  if (this.closed) {
70
67
  return null;
71
68
  }
@@ -133,12 +130,14 @@ export class DataStream extends BaseObserver {
133
130
  return Array.from(this.listeners.values()).some((l) => !!l.data);
134
131
  }
135
132
  async _processQueue() {
136
- if (!this.dataQueue.length || this.isClosed || !this.hasDataReader()) {
133
+ if (this.isClosed || !this.hasDataReader()) {
137
134
  Promise.resolve().then(() => (this.processingPromise = null));
138
135
  return;
139
136
  }
140
- const data = this.dataQueue.shift();
141
- await this.iterateAsyncErrored(async (l) => l.data?.(data));
137
+ if (this.dataQueue.length) {
138
+ const data = this.dataQueue.shift();
139
+ await this.iterateAsyncErrored(async (l) => l.data?.(data));
140
+ }
142
141
  if (this.dataQueue.length <= this.lowWatermark) {
143
142
  await this.iterateAsyncErrored(async (l) => l.lowWater?.());
144
143
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@powersync/common",
3
- "version": "1.23.0",
3
+ "version": "1.25.0",
4
4
  "publishConfig": {
5
5
  "registry": "https://registry.npmjs.org/",
6
6
  "access": "public"
@@ -52,8 +52,6 @@
52
52
  "rsocket-core": "1.0.0-alpha.3",
53
53
  "rsocket-websocket-client": "1.0.0-alpha.3",
54
54
  "text-encoding": "^0.7.0",
55
- "typescript": "^5.5.3",
56
- "vitest": "^2.0.5",
57
55
  "web-streams-polyfill": "3.2.1"
58
56
  },
59
57
  "scripts": {