sliftutils 1.4.81 → 1.4.83

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,8 +4,12 @@ import { Database } from "./Database";
4
4
  // structuredClone keeps typed arrays / Maps / undefined intact through the round-trip.
5
5
  const transactionCbor = new cborx.Encoder({ structuredClone: true });
6
6
 
7
- // On the store: files keyed by a monotonic number, each a CBOR array of transactions; a transaction sets one key (value undefined = delete) and carries a global sequence number. Replaying every transaction in sequence order (last write wins) gives the current map. Mutations append a file, then fold the whole set into one once enough accumulate.
8
- export type TransactionSetStore = { [fileNumber: string]: Uint8Array };
7
+ // On the store: files keyed by a monotonic number, each a CBOR array of transactions; a transaction sets one key (value undefined = delete) and carries a global sequence number. Replaying every transaction in sequence order (last write wins) gives the current map. Mutations append a file, then fold the whole set into one once enough accumulate. Value is phantom — it only records what the encoded values deserialize to.
8
+ declare const valueTag: unique symbol;
9
+ export type TransactionSetStore<Value> = {
10
+ [fileNumber: string]: Uint8Array;
11
+ [valueTag]?: Value;
12
+ };
9
13
 
10
14
  type Transaction = { sequence: number; key: string; value: unknown };
11
15
 
@@ -32,13 +36,15 @@ function decodeBuffers(buffers: Uint8Array[]): Transaction[] {
32
36
  return transactions;
33
37
  }
34
38
 
35
- function readTransactionFiles(
36
- database: Database<TransactionSetStore>,
39
+ function readTransactionFiles<Value>(
40
+ database: Database<TransactionSetStore<Value>>,
37
41
  ): { fileKeys: string[]; transactions: Transaction[] } | undefined {
38
- const snapshot = database.readData(store => ({
39
- fileKeys: Object.keys(store),
40
- buffers: Object.values(store),
41
- }));
42
+ const snapshot = database.readData(store => {
43
+ // A never-written / deleted set reads as empty (vs the whole readData being undefined = not synced).
44
+ const present = store as TransactionSetStore<Value> | undefined;
45
+ if (!present) return { fileKeys: [] as string[], buffers: [] as Uint8Array[] };
46
+ return { fileKeys: Object.keys(present), buffers: Object.values(present) };
47
+ });
42
48
  if (!snapshot) return undefined;
43
49
  return { fileKeys: snapshot.fileKeys, transactions: decodeBuffers(snapshot.buffers) };
44
50
  }
@@ -58,14 +64,17 @@ function replay(transactions: Transaction[]): Map<string, unknown> {
58
64
  }
59
65
 
60
66
  // Deletes old files by their ORIGINAL string key, so a stray non-numeric key still gets removed.
61
- function compactTransactions(
62
- database: Database<TransactionSetStore>,
67
+ function compactTransactions<Value>(
68
+ database: Database<TransactionSetStore<Value>>,
63
69
  allTransactions: Transaction[],
64
70
  oldFileKeys: string[],
65
71
  newFileNumber: number,
66
72
  ): void {
67
73
  const finalState = replay(allTransactions);
68
- const merged: Transaction[] = [...finalState].map(([key, value], index) => ({ sequence: index, key, value }));
74
+ const merged: Transaction[] = [];
75
+ for (const key of finalState.keys()) {
76
+ merged.push({ sequence: merged.length, key, value: finalState.get(key) });
77
+ }
69
78
  const encoded = transactionCbor.encode(merged) as Uint8Array;
70
79
  database.writeData(store => store[String(newFileNumber)], encoded);
71
80
  for (const fileKey of oldFileKeys) {
@@ -74,28 +83,29 @@ function compactTransactions(
74
83
  }
75
84
 
76
85
  export function transactionRead<Value>(
77
- database: Database<TransactionSetStore>,
86
+ database: Database<TransactionSetStore<Value>>,
78
87
  ): Map<string, Value> | undefined {
79
88
  const files = readTransactionFiles(database);
80
89
  if (!files) return undefined;
81
90
  return replay(files.transactions) as Map<string, Value>;
82
91
  }
83
92
 
84
- // Replay an already-read store object (no read of its own), so a caller can fetch many sets in one batched read and then materialize each.
85
- export function replayTransactionStore<Value>(store: TransactionSetStore | undefined): Map<string, Value> {
93
+ // Replay an already-read store (no read of its own), so a caller can fetch many sets in one batched read and materialize each.
94
+ export function replayTransactionStore<Value>(store: TransactionSetStore<Value> | undefined): Map<string, Value> {
86
95
  if (!store) return new Map();
87
96
  return replay(decodeBuffers(Object.values(store))) as Map<string, Value>;
88
97
  }
89
98
 
99
+ // Fire-and-forget: no caller should care whether this "succeeded". If the set isn't synced for reading
100
+ // yet it simply does nothing this pass and the caller's retry wrapper re-runs the whole operation later.
90
101
  export function transactionMutate<Value>(
91
- database: Database<TransactionSetStore>,
102
+ database: Database<TransactionSetStore<Value>>,
92
103
  transactions: { key: string; value: Value | undefined }[],
93
104
  compactAfterFiles: number = DEFAULT_COMPACT_AFTER_FILES,
94
- ): true | undefined {
105
+ ): void {
106
+ if (!transactions.length) return;
95
107
  const files = readTransactionFiles(database);
96
- if (!files) return undefined;
97
- if (!transactions.length) return true;
98
-
108
+ if (!files) return;
99
109
  const baseSequence = maxNumber(files.transactions.map(transaction => transaction.sequence), -1) + 1;
100
110
  const incoming: Transaction[] = transactions.map((transaction, index) => ({
101
111
  sequence: baseSequence + index,
@@ -103,12 +113,23 @@ export function transactionMutate<Value>(
103
113
  value: transaction.value,
104
114
  }));
105
115
  const nextFileNumber = maxNumber(files.fileKeys.map(fileKey => Number(fileKey)), -1) + 1;
106
-
107
116
  if (files.fileKeys.length + 1 >= compactAfterFiles) {
108
- compactTransactions(database, [...files.transactions, ...incoming], files.fileKeys, nextFileNumber);
117
+ compactTransactions(database, files.transactions.concat(incoming), files.fileKeys, nextFileNumber);
109
118
  } else {
110
119
  const encoded = transactionCbor.encode(incoming) as Uint8Array;
111
120
  database.writeData(store => store[String(nextFileNumber)], encoded);
112
121
  }
113
- return true;
122
+ }
123
+
124
+ // Fire-and-forget like transactionMutate: clears the whole set by deleting every file. deleteData only removes
125
+ // a primitive leaf (not a store object), so we delete each file key individually. No-ops (the retry re-runs)
126
+ // if the store isn't synced yet.
127
+ export function transactionDelete<Value>(
128
+ database: Database<TransactionSetStore<Value>>,
129
+ ): void {
130
+ const files = readTransactionFiles(database);
131
+ if (!files) return;
132
+ for (const fileKey of files.fileKeys) {
133
+ database.deleteData(store => store[fileKey]);
134
+ }
114
135
  }