sliftutils 1.7.99 → 1.7.101

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.
package/index.d.ts CHANGED
@@ -910,7 +910,8 @@ declare module "sliftutils/storage/ArchivesDisk" {
910
910
  private collectFiles;
911
911
  setLargeFile(config: SetLargeFileConfig): Promise<void>;
912
912
  startLargeUpload(): Promise<string>;
913
- appendLargeUpload(id: string, data: Buffer): Promise<void>;
913
+ /** offset makes the write POSITIONAL instead of appending: a retried part lands exactly where the failed attempt would have, so part retries are idempotent. One upload must use either appends or offsets throughout, never both - the cached handle keeps its first flags, and O_APPEND ignores the position argument. */
914
+ appendLargeUpload(id: string, data: Buffer, offset?: number): Promise<void>;
914
915
  finishLargeUpload(id: string, key: string, lastModified?: number): Promise<void>;
915
916
  cancelLargeUpload(id: string): Promise<void>;
916
917
  getURL(path: string): Promise<string>;
@@ -2169,6 +2170,8 @@ declare module "sliftutils/storage/IArchives" {
2169
2170
  internal?: boolean;
2170
2171
  /** Also return size-0 results (tombstones - an empty file IS a missing file) instead of treating them as absent. Off by default, matching getInfo's flag of the same name. Synchronization passes this so a DELETED file (with its write time) is distinguishable from a file that never existed. */
2171
2172
  includeTombstones?: boolean;
2173
+ /** How many extra times the WHOLE operation is retried after every source in a pass failed - any error counts (the wrong-window/route markers still get their config re-resolve first). Only applies to fallback dispatch (multi-source, not noFallbacks), where it defaults to 3; the noFallbacks/write-node path already retries on its own deadline. Multi-part uploads additionally retry per part regardless of this. */
2174
+ retries?: number;
2172
2175
  };
2173
2176
  export type FindConfig = {
2174
2177
  shallow?: boolean;
@@ -2185,12 +2188,16 @@ declare module "sliftutils/storage/IArchives" {
2185
2188
  noChecks?: boolean;
2186
2189
  /** See SetConfig.fallbacks. */
2187
2190
  fallbacks?: boolean;
2191
+ /** See GetConfig.retries. */
2192
+ retries?: number;
2188
2193
  };
2189
2194
  export type GetInfoConfig = {
2190
2195
  /** Also report size-0 entries (tombstones - an empty file IS a missing file). Off by default, so a deleted key reports undefined, matching get. Synchronization-style callers pass this when they need a deletion's write time (e.g. to compare it against a write they are about to make). */
2191
2196
  includeTombstones?: boolean;
2192
2197
  /** See GetConfig.noFallbacks: answer ONLY from the primary source (the one writes would target) instead of falling back across the redundant sources. */
2193
2198
  noFallbacks?: boolean;
2199
+ /** See GetConfig.retries. */
2200
+ retries?: number;
2194
2201
  };
2195
2202
  export type ChangesAfterConfig = {
2196
2203
  time: number;
@@ -2207,6 +2214,8 @@ declare module "sliftutils/storage/IArchives" {
2207
2214
  internal?: boolean;
2208
2215
  /** Writes normally go ONLY to the write node (the first current-window source covering the key), retrying it even while it is down - consistent, but unavailable when that node is. With fallbacks, the write node is still tried first, but on failure the write lands on the next current-window source covering the key (synchronization moves it to the write node later) - availability at the cost of reads possibly missing the write until it propagates. Single-source archives ignore the flag. */
2209
2216
  fallbacks?: boolean;
2217
+ /** See GetConfig.retries. */
2218
+ retries?: number;
2210
2219
  };
2211
2220
  /** setLargeFile's config: a SetConfig (it IS a set - the same immutability, ordering, internal, and fallbacks rules apply) plus the stream carrying the bytes. */
2212
2221
  export type SetLargeFileConfig = SetConfig & {
@@ -2456,7 +2465,7 @@ declare module "sliftutils/storage/LogMap" {
2456
2465
  time: number;
2457
2466
  changedAt: number;
2458
2467
  };
2459
- export declare class LogMap<T> {
2468
+ export declare class TransactionFile<T> {
2460
2469
  private filePath;
2461
2470
  constructor(filePath: string);
2462
2471
  private values;
@@ -2622,6 +2631,66 @@ declare module "sliftutils/storage/StorageObservableAsync" {
2622
2631
 
2623
2632
  }
2624
2633
 
2634
+ declare module "sliftutils/storage/TransactionFile" {
2635
+ /** A live value: what was stored, when it was written (the caller's ordering), and when we last changed it (ours). */
2636
+ export type LogEntry<T> = {
2637
+ value: T;
2638
+ time: number;
2639
+ changedAt: number;
2640
+ };
2641
+ /** A deleted key: when it was deleted, and when we learned. The value is gone; the time is the point of a tombstone. */
2642
+ export type LogTombstone = {
2643
+ time: number;
2644
+ changedAt: number;
2645
+ };
2646
+ export declare class TransactionFile<T> {
2647
+ private filePath;
2648
+ constructor(filePath: string);
2649
+ private values;
2650
+ private deleted;
2651
+ private logRecords;
2652
+ private pending;
2653
+ private flushTimer;
2654
+ private writeChain;
2655
+ /** Reads the log and replays it into memory. Every other method assumes this has finished. */
2656
+ load: {
2657
+ (): Promise<void>;
2658
+ reset(): void;
2659
+ set(newValue: Promise<void>): void;
2660
+ };
2661
+ /** The live value, or undefined when the key does not exist here (deleted included - a deletion is an absence, see getDeleted for its time). */
2662
+ get(key: string): LogEntry<T> | undefined;
2663
+ /** When the key was deleted, if it was. Absent both here and in get means we have never heard of it. */
2664
+ getDeleted(key: string): LogTombstone | undefined;
2665
+ /** The time the key last changed either way, or 0 if we have never heard of it - what a new write has to beat. */
2666
+ timeOf(key: string): number;
2667
+ /** O(1), and counts only what exists. */
2668
+ get size(): number;
2669
+ get deletedSize(): number;
2670
+ /** Live values only. Live, in insertion order - deleting during iteration is safe (JS skips entries removed before they are reached), which is what the passes that walk everything and prune as they go rely on. */
2671
+ entries(): IterableIterator<[string, LogEntry<T>]>;
2672
+ /** The tombstones, which is a much smaller walk than the values - so expiring them, or listing what was deleted since some time, costs what it should. */
2673
+ deletedEntries(): IterableIterator<[string, LogTombstone]>;
2674
+ /** Stores a value as of `time`. Returns false when something at least as new is already here, in which case nothing changed - an out-of-order write is not an error, it is just late. */
2675
+ set(key: string, value: T, time: number): boolean;
2676
+ /** Deletes as of `time`, keeping the tombstone. Returns false when something at least as new is already here. */
2677
+ delete(key: string, time: number): boolean;
2678
+ /** Forgets the key entirely, tombstone included - for a tombstone old enough that nobody needs to hear about the deletion any more, and for an entry that turned out never to have existed. Not a deletion: it leaves nothing behind to propagate. */
2679
+ purge(key: string): void;
2680
+ private applySet;
2681
+ private applyDelete;
2682
+ private append;
2683
+ private scheduleFlush;
2684
+ /** Writes everything pending (rewriting the log first if it has grown too far past what it describes). */
2685
+ flush(): Promise<void>;
2686
+ private directory;
2687
+ private write;
2688
+ private appendPending;
2689
+ private compact;
2690
+ }
2691
+
2692
+ }
2693
+
2625
2694
  declare module "sliftutils/storage/TransactionStorage" {
2626
2695
  /// <reference types="node" />
2627
2696
  /// <reference types="node" />
@@ -3241,7 +3310,7 @@ declare module "sliftutils/storage/remoteStorage/blobStore" {
3241
3310
  import { StoreSync } from "./storeSync";
3242
3311
  import { StoreConfig } from "./storeConfig";
3243
3312
  export declare const WINDOW_END_FLUSH_MARGIN: number;
3244
- /** What we store about a file. Its times are not in here: the index keeps those for every key, deleted ones included (see LogMap). */
3313
+ /** What we store about a file. Its times are not in here: the index keeps those for every key, deleted ones included (see TransactionFile). */
3245
3314
  type IndexValue = {
3246
3315
  size: number;
3247
3316
  sourcesListIndex: number;
@@ -3425,6 +3494,7 @@ declare module "sliftutils/storage/remoteStorage/blobStore" {
3425
3494
  appendLargeUpload(config: {
3426
3495
  id: string;
3427
3496
  data: Buffer;
3497
+ offset?: number;
3428
3498
  }): Promise<void>;
3429
3499
  finishLargeUpload(config: {
3430
3500
  id: string;
@@ -4294,6 +4364,7 @@ declare module "sliftutils/storage/remoteStorage/storageController" {
4294
4364
  uploadPart: (config: {
4295
4365
  uploadId: string;
4296
4366
  data: Buffer;
4367
+ offset?: number;
4297
4368
  }) => Promise<void>;
4298
4369
  finishLargeFile: (config: {
4299
4370
  uploadId: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sliftutils",
3
- "version": "1.7.99",
3
+ "version": "1.7.101",
4
4
  "main": "index.js",
5
5
  "license": "MIT",
6
6
  "files": [
@@ -37,7 +37,8 @@ export declare class ArchivesDisk implements IArchives {
37
37
  private collectFiles;
38
38
  setLargeFile(config: SetLargeFileConfig): Promise<void>;
39
39
  startLargeUpload(): Promise<string>;
40
- appendLargeUpload(id: string, data: Buffer): Promise<void>;
40
+ /** offset makes the write POSITIONAL instead of appending: a retried part lands exactly where the failed attempt would have, so part retries are idempotent. One upload must use either appends or offsets throughout, never both - the cached handle keeps its first flags, and O_APPEND ignores the position argument. */
41
+ appendLargeUpload(id: string, data: Buffer, offset?: number): Promise<void>;
41
42
  finishLargeUpload(id: string, key: string, lastModified?: number): Promise<void>;
42
43
  cancelLargeUpload(id: string): Promise<void>;
43
44
  getURL(path: string): Promise<string>;
@@ -322,13 +322,19 @@ export class ArchivesDisk implements IArchives {
322
322
  this.largeUploads.set(id, { tmpPath: path.join(this.uploadsDir, `upload_${id}.tmp`) });
323
323
  return id;
324
324
  }
325
- public async appendLargeUpload(id: string, data: Buffer): Promise<void> {
325
+ /** offset makes the write POSITIONAL instead of appending: a retried part lands exactly where the failed attempt would have, so part retries are idempotent. One upload must use either appends or offsets throughout, never both - the cached handle keeps its first flags, and O_APPEND ignores the position argument. */
326
+ public async appendLargeUpload(id: string, data: Buffer, offset?: number): Promise<void> {
326
327
  let upload = this.largeUploads.get(id);
327
328
  if (!upload) throw new Error(`Unknown large upload ${id}`);
328
329
  const tmpPath = upload.tmpPath;
329
330
  await this.handles.run(tmpPath, async () => {
330
- let handle = await this.handles.getHandle(tmpPath, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_APPEND);
331
- await handle.write(data, 0, data.length);
331
+ if (offset === undefined) {
332
+ let handle = await this.handles.getHandle(tmpPath, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_APPEND);
333
+ await handle.write(data, 0, data.length);
334
+ return;
335
+ }
336
+ let handle = await this.handles.getHandle(tmpPath, fs.constants.O_WRONLY | fs.constants.O_CREAT);
337
+ await handle.write(data, 0, data.length, offset);
332
338
  });
333
339
  }
334
340
  public async finishLargeUpload(id: string, key: string, lastModified?: number): Promise<void> {
@@ -75,6 +75,8 @@ export type GetConfig = {
75
75
  internal?: boolean;
76
76
  /** Also return size-0 results (tombstones - an empty file IS a missing file) instead of treating them as absent. Off by default, matching getInfo's flag of the same name. Synchronization passes this so a DELETED file (with its write time) is distinguishable from a file that never existed. */
77
77
  includeTombstones?: boolean;
78
+ /** How many extra times the WHOLE operation is retried after every source in a pass failed - any error counts (the wrong-window/route markers still get their config re-resolve first). Only applies to fallback dispatch (multi-source, not noFallbacks), where it defaults to 3; the noFallbacks/write-node path already retries on its own deadline. Multi-part uploads additionally retry per part regardless of this. */
79
+ retries?: number;
78
80
  };
79
81
  export type FindConfig = {
80
82
  shallow?: boolean;
@@ -91,12 +93,16 @@ export type DelConfig = {
91
93
  noChecks?: boolean;
92
94
  /** See SetConfig.fallbacks. */
93
95
  fallbacks?: boolean;
96
+ /** See GetConfig.retries. */
97
+ retries?: number;
94
98
  };
95
99
  export type GetInfoConfig = {
96
100
  /** Also report size-0 entries (tombstones - an empty file IS a missing file). Off by default, so a deleted key reports undefined, matching get. Synchronization-style callers pass this when they need a deletion's write time (e.g. to compare it against a write they are about to make). */
97
101
  includeTombstones?: boolean;
98
102
  /** See GetConfig.noFallbacks: answer ONLY from the primary source (the one writes would target) instead of falling back across the redundant sources. */
99
103
  noFallbacks?: boolean;
104
+ /** See GetConfig.retries. */
105
+ retries?: number;
100
106
  };
101
107
  export type ChangesAfterConfig = {
102
108
  time: number;
@@ -113,6 +119,8 @@ export type SetConfig = {
113
119
  internal?: boolean;
114
120
  /** Writes normally go ONLY to the write node (the first current-window source covering the key), retrying it even while it is down - consistent, but unavailable when that node is. With fallbacks, the write node is still tried first, but on failure the write lands on the next current-window source covering the key (synchronization moves it to the write node later) - availability at the cost of reads possibly missing the write until it propagates. Single-source archives ignore the flag. */
115
121
  fallbacks?: boolean;
122
+ /** See GetConfig.retries. */
123
+ retries?: number;
116
124
  };
117
125
  /** setLargeFile's config: a SetConfig (it IS a set - the same immutability, ordering, internal, and fallbacks rules apply) plus the stream carrying the bytes. */
118
126
  export type SetLargeFileConfig = SetConfig & {
@@ -110,6 +110,8 @@ export type GetConfig = {
110
110
  internal?: boolean;
111
111
  /** Also return size-0 results (tombstones - an empty file IS a missing file) instead of treating them as absent. Off by default, matching getInfo's flag of the same name. Synchronization passes this so a DELETED file (with its write time) is distinguishable from a file that never existed. */
112
112
  includeTombstones?: boolean;
113
+ /** How many extra times the WHOLE operation is retried after every source in a pass failed - any error counts (the wrong-window/route markers still get their config re-resolve first). Only applies to fallback dispatch (multi-source, not noFallbacks), where it defaults to 3; the noFallbacks/write-node path already retries on its own deadline. Multi-part uploads additionally retry per part regardless of this. */
114
+ retries?: number;
113
115
  };
114
116
 
115
117
  export type FindConfig = {
@@ -128,6 +130,8 @@ export type DelConfig = {
128
130
  noChecks?: boolean;
129
131
  /** See SetConfig.fallbacks. */
130
132
  fallbacks?: boolean;
133
+ /** See GetConfig.retries. */
134
+ retries?: number;
131
135
  };
132
136
 
133
137
  export type GetInfoConfig = {
@@ -135,6 +139,8 @@ export type GetInfoConfig = {
135
139
  includeTombstones?: boolean;
136
140
  /** See GetConfig.noFallbacks: answer ONLY from the primary source (the one writes would target) instead of falling back across the redundant sources. */
137
141
  noFallbacks?: boolean;
142
+ /** See GetConfig.retries. */
143
+ retries?: number;
138
144
  };
139
145
 
140
146
  export type ChangesAfterConfig = {
@@ -153,6 +159,8 @@ export type SetConfig = {
153
159
  internal?: boolean;
154
160
  /** Writes normally go ONLY to the write node (the first current-window source covering the key), retrying it even while it is down - consistent, but unavailable when that node is. With fallbacks, the write node is still tried first, but on failure the write lands on the next current-window source covering the key (synchronization moves it to the write node later) - availability at the cost of reads possibly missing the write until it propagates. Single-source archives ignore the flag. */
155
161
  fallbacks?: boolean;
162
+ /** See GetConfig.retries. */
163
+ retries?: number;
156
164
  };
157
165
 
158
166
  /** setLargeFile's config: a SetConfig (it IS a set - the same immutability, ordering, internal, and fallbacks rules apply) plus the stream carrying the bytes. */
@@ -9,7 +9,7 @@ export type LogTombstone = {
9
9
  time: number;
10
10
  changedAt: number;
11
11
  };
12
- export declare class LogMap<T> {
12
+ export declare class TransactionFile<T> {
13
13
  private filePath;
14
14
  constructor(filePath: string);
15
15
  private values;
@@ -0,0 +1,56 @@
1
+ /** A live value: what was stored, when it was written (the caller's ordering), and when we last changed it (ours). */
2
+ export type LogEntry<T> = {
3
+ value: T;
4
+ time: number;
5
+ changedAt: number;
6
+ };
7
+ /** A deleted key: when it was deleted, and when we learned. The value is gone; the time is the point of a tombstone. */
8
+ export type LogTombstone = {
9
+ time: number;
10
+ changedAt: number;
11
+ };
12
+ export declare class TransactionFile<T> {
13
+ private filePath;
14
+ constructor(filePath: string);
15
+ private values;
16
+ private deleted;
17
+ private logRecords;
18
+ private pending;
19
+ private flushTimer;
20
+ private writeChain;
21
+ /** Reads the log and replays it into memory. Every other method assumes this has finished. */
22
+ load: {
23
+ (): Promise<void>;
24
+ reset(): void;
25
+ set(newValue: Promise<void>): void;
26
+ };
27
+ /** The live value, or undefined when the key does not exist here (deleted included - a deletion is an absence, see getDeleted for its time). */
28
+ get(key: string): LogEntry<T> | undefined;
29
+ /** When the key was deleted, if it was. Absent both here and in get means we have never heard of it. */
30
+ getDeleted(key: string): LogTombstone | undefined;
31
+ /** The time the key last changed either way, or 0 if we have never heard of it - what a new write has to beat. */
32
+ timeOf(key: string): number;
33
+ /** O(1), and counts only what exists. */
34
+ get size(): number;
35
+ get deletedSize(): number;
36
+ /** Live values only. Live, in insertion order - deleting during iteration is safe (JS skips entries removed before they are reached), which is what the passes that walk everything and prune as they go rely on. */
37
+ entries(): IterableIterator<[string, LogEntry<T>]>;
38
+ /** The tombstones, which is a much smaller walk than the values - so expiring them, or listing what was deleted since some time, costs what it should. */
39
+ deletedEntries(): IterableIterator<[string, LogTombstone]>;
40
+ /** Stores a value as of `time`. Returns false when something at least as new is already here, in which case nothing changed - an out-of-order write is not an error, it is just late. */
41
+ set(key: string, value: T, time: number): boolean;
42
+ /** Deletes as of `time`, keeping the tombstone. Returns false when something at least as new is already here. */
43
+ delete(key: string, time: number): boolean;
44
+ /** Forgets the key entirely, tombstone included - for a tombstone old enough that nobody needs to hear about the deletion any more, and for an entry that turned out never to have existed. Not a deletion: it leaves nothing behind to propagate. */
45
+ purge(key: string): void;
46
+ private applySet;
47
+ private applyDelete;
48
+ private append;
49
+ private scheduleFlush;
50
+ /** Writes everything pending (rewriting the log first if it has grown too far past what it describes). */
51
+ flush(): Promise<void>;
52
+ private directory;
53
+ private write;
54
+ private appendPending;
55
+ private compact;
56
+ }
@@ -18,18 +18,21 @@ export type LogEntry<T> = { value: T; time: number; changedAt: number };
18
18
  /** A deleted key: when it was deleted, and when we learned. The value is gone; the time is the point of a tombstone. */
19
19
  export type LogTombstone = { time: number; changedAt: number };
20
20
 
21
- // One line of the log. JSON.stringify escapes newlines inside keys and values, so a record can never contain the separator.
21
+ // One line of the log, with single-letter field names because there is one record per set/delete/purge ever made and the field names are most of a record's overhead. JSON.stringify escapes newlines inside keys and values, so a record can never contain the separator.
22
22
  type LogRecord<T> = {
23
+ // key
23
24
  k: string;
24
- // The caller's time for this change - what ordering is decided by
25
+ // time, lastModified time of the file, or the delete time, or the purge time
25
26
  t: number;
26
- // A set carries the value; a delete carries d; a purge carries p (forget the key entirely, tombstone included)
27
+ // value
27
28
  v?: T;
29
+ // deleted, the file on disk is deleted, preserved when we compact
28
30
  d?: 1;
31
+ // purged, the file on disk is deleted, not preserved when we compact
29
32
  p?: 1;
30
33
  };
31
34
 
32
- export class LogMap<T> {
35
+ export class TransactionFile<T> {
33
36
  constructor(private filePath: string) { }
34
37
 
35
38
  private values = new Map<string, LogEntry<T>>();
@@ -629,7 +629,10 @@ export class ArchivesBackblaze implements IArchives {
629
629
  }
630
630
  }
631
631
 
632
- throw err;
632
+ // EVERYTHING else retries too (bounded by `retries`): backblaze fails a small fraction of requests with ever-new error shapes, and aborting a multi-part upload (or a full sync) on one unmatched error costs the whole transfer. A genuinely permanent error just pays the few retry delays before surfacing.
633
+ console.warn(`[${context}] Unrecognized error, retrying in 5s (${retries} left): ${err.message}`);
634
+ await delay(5000);
635
+ return this.apiRetryLogic(context, fnc, retries - 1);
633
636
  }
634
637
  }
635
638