sliftutils 1.4.0 → 1.4.2

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
@@ -813,20 +813,37 @@ declare module "sliftutils/storage/BulkDatabase2/BulkDatabase2" {
813
813
  getKeys(): Promise<string[]>;
814
814
  /** One field's value for a key, or undefined if the key/column isn't set or the key is deleted. */
815
815
  getSingleField<Column extends keyof T>(key: string, column: Column): Promise<T[Column] | undefined>;
816
- /** A whole column as {key, value} for every live key. */
816
+ /**
817
+ * Like getSingleField but returns { key, value, time } (the same shape a getColumn entry has), where
818
+ * time is roughly when the value last changed. undefined only when the key isn't present/live.
819
+ */
820
+ getSingleFieldObj<Column extends keyof T>(key: string, column: Column): Promise<{
821
+ key: string;
822
+ value: T[Column];
823
+ time: number;
824
+ } | undefined>;
825
+ /** A whole column as { key, value, time } for every live key (time ≈ when each value last changed). */
817
826
  getColumn<Column extends keyof T>(column: Column): Promise<{
818
827
  key: string;
819
828
  value: T[Column];
829
+ time: number;
820
830
  }[]>;
821
831
  /**
822
832
  * Synchronous, reactive read of one field. Returns undefined while the base value is still loading
823
833
  * (and re-renders once it arrives, under a mobx observer); reflects pending writes immediately.
824
834
  */
825
835
  getSingleFieldSync<Column extends keyof T>(key: string, column: Column): T[Column] | undefined;
826
- /** Synchronous, reactive read of a whole column. undefined while still loading. */
836
+ /** Sync, reactive counterpart of getSingleFieldObj: { key, value, time } once loaded, else undefined. */
837
+ getSingleFieldObjSync<Column extends keyof T>(key: string, column: Column): {
838
+ key: string;
839
+ value: T[Column];
840
+ time: number;
841
+ } | undefined;
842
+ /** Synchronous, reactive read of a whole column ({ key, value, time }). undefined while still loading. */
827
843
  getColumnSync<Column extends keyof T>(column: Column): {
828
844
  key: string;
829
845
  value: T[Column];
846
+ time: number;
830
847
  }[] | undefined;
831
848
  /** The columns present on disk and their byte sizes (no row data read). */
832
849
  getColumnInfo(): Promise<BulkColumnInfo[]>;
@@ -931,9 +948,15 @@ declare module "sliftutils/storage/BulkDatabase2/BulkDatabaseBase" {
931
948
  private formatInfo;
932
949
  private patchColumn;
933
950
  getSingleField<Column extends keyof T>(key: string, column: Column): Promise<T[Column] | undefined>;
951
+ getSingleFieldObj<Column extends keyof T>(key: string, column: Column): Promise<{
952
+ key: string;
953
+ value: T[Column];
954
+ time: number;
955
+ } | undefined>;
934
956
  getColumn<Column extends keyof T>(column: Column): Promise<{
935
957
  key: string;
936
958
  value: T[Column];
959
+ time: number;
937
960
  }[]>;
938
961
  getKeys(): Promise<string[]>;
939
962
  private baseColumns;
@@ -943,9 +966,15 @@ declare module "sliftutils/storage/BulkDatabase2/BulkDatabaseBase" {
943
966
  private ensureBaseColumn;
944
967
  private ensureBaseField;
945
968
  getSingleFieldSync<Column extends keyof T>(key: string, column: Column): T[Column] | undefined;
969
+ getSingleFieldObjSync<Column extends keyof T>(key: string, column: Column): {
970
+ key: string;
971
+ value: T[Column];
972
+ time: number;
973
+ } | undefined;
946
974
  getColumnSync<Column extends keyof T>(column: Column): {
947
975
  key: string;
948
976
  value: T[Column];
977
+ time: number;
949
978
  }[] | undefined;
950
979
  getColumnInfo(): Promise<{
951
980
  column: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sliftutils",
3
- "version": "1.4.0",
3
+ "version": "1.4.2",
4
4
  "main": "index.js",
5
5
  "license": "MIT",
6
6
  "files": [
@@ -48,20 +48,37 @@ export interface IBulkDatabase2<T extends {
48
48
  getKeys(): Promise<string[]>;
49
49
  /** One field's value for a key, or undefined if the key/column isn't set or the key is deleted. */
50
50
  getSingleField<Column extends keyof T>(key: string, column: Column): Promise<T[Column] | undefined>;
51
- /** A whole column as {key, value} for every live key. */
51
+ /**
52
+ * Like getSingleField but returns { key, value, time } (the same shape a getColumn entry has), where
53
+ * time is roughly when the value last changed. undefined only when the key isn't present/live.
54
+ */
55
+ getSingleFieldObj<Column extends keyof T>(key: string, column: Column): Promise<{
56
+ key: string;
57
+ value: T[Column];
58
+ time: number;
59
+ } | undefined>;
60
+ /** A whole column as { key, value, time } for every live key (time ≈ when each value last changed). */
52
61
  getColumn<Column extends keyof T>(column: Column): Promise<{
53
62
  key: string;
54
63
  value: T[Column];
64
+ time: number;
55
65
  }[]>;
56
66
  /**
57
67
  * Synchronous, reactive read of one field. Returns undefined while the base value is still loading
58
68
  * (and re-renders once it arrives, under a mobx observer); reflects pending writes immediately.
59
69
  */
60
70
  getSingleFieldSync<Column extends keyof T>(key: string, column: Column): T[Column] | undefined;
61
- /** Synchronous, reactive read of a whole column. undefined while still loading. */
71
+ /** Sync, reactive counterpart of getSingleFieldObj: { key, value, time } once loaded, else undefined. */
72
+ getSingleFieldObjSync<Column extends keyof T>(key: string, column: Column): {
73
+ key: string;
74
+ value: T[Column];
75
+ time: number;
76
+ } | undefined;
77
+ /** Synchronous, reactive read of a whole column ({ key, value, time }). undefined while still loading. */
62
78
  getColumnSync<Column extends keyof T>(column: Column): {
63
79
  key: string;
64
80
  value: T[Column];
81
+ time: number;
65
82
  }[] | undefined;
66
83
  /** The columns present on disk and their byte sizes (no row data read). */
67
84
  getColumnInfo(): Promise<BulkColumnInfo[]>;
@@ -47,16 +47,23 @@ export interface IBulkDatabase2<T extends { key: string }> {
47
47
  getKeys(): Promise<string[]>;
48
48
  /** One field's value for a key, or undefined if the key/column isn't set or the key is deleted. */
49
49
  getSingleField<Column extends keyof T>(key: string, column: Column): Promise<T[Column] | undefined>;
50
- /** A whole column as {key, value} for every live key. */
51
- getColumn<Column extends keyof T>(column: Column): Promise<{ key: string; value: T[Column] }[]>;
50
+ /**
51
+ * Like getSingleField but returns { key, value, time } (the same shape a getColumn entry has), where
52
+ * time is roughly when the value last changed. undefined only when the key isn't present/live.
53
+ */
54
+ getSingleFieldObj<Column extends keyof T>(key: string, column: Column): Promise<{ key: string; value: T[Column]; time: number } | undefined>;
55
+ /** A whole column as { key, value, time } for every live key (time ≈ when each value last changed). */
56
+ getColumn<Column extends keyof T>(column: Column): Promise<{ key: string; value: T[Column]; time: number }[]>;
52
57
 
53
58
  /**
54
59
  * Synchronous, reactive read of one field. Returns undefined while the base value is still loading
55
60
  * (and re-renders once it arrives, under a mobx observer); reflects pending writes immediately.
56
61
  */
57
62
  getSingleFieldSync<Column extends keyof T>(key: string, column: Column): T[Column] | undefined;
58
- /** Synchronous, reactive read of a whole column. undefined while still loading. */
59
- getColumnSync<Column extends keyof T>(column: Column): { key: string; value: T[Column] }[] | undefined;
63
+ /** Sync, reactive counterpart of getSingleFieldObj: { key, value, time } once loaded, else undefined. */
64
+ getSingleFieldObjSync<Column extends keyof T>(key: string, column: Column): { key: string; value: T[Column]; time: number } | undefined;
65
+ /** Synchronous, reactive read of a whole column ({ key, value, time }). undefined while still loading. */
66
+ getColumnSync<Column extends keyof T>(column: Column): { key: string; value: T[Column]; time: number }[] | undefined;
60
67
 
61
68
  /** The columns present on disk and their byte sizes (no row data read). */
62
69
  getColumnInfo(): Promise<BulkColumnInfo[]>;
@@ -72,9 +72,15 @@ export declare class BulkDatabaseBase<T extends {
72
72
  private formatInfo;
73
73
  private patchColumn;
74
74
  getSingleField<Column extends keyof T>(key: string, column: Column): Promise<T[Column] | undefined>;
75
+ getSingleFieldObj<Column extends keyof T>(key: string, column: Column): Promise<{
76
+ key: string;
77
+ value: T[Column];
78
+ time: number;
79
+ } | undefined>;
75
80
  getColumn<Column extends keyof T>(column: Column): Promise<{
76
81
  key: string;
77
82
  value: T[Column];
83
+ time: number;
78
84
  }[]>;
79
85
  getKeys(): Promise<string[]>;
80
86
  private baseColumns;
@@ -84,9 +90,15 @@ export declare class BulkDatabaseBase<T extends {
84
90
  private ensureBaseColumn;
85
91
  private ensureBaseField;
86
92
  getSingleFieldSync<Column extends keyof T>(key: string, column: Column): T[Column] | undefined;
93
+ getSingleFieldObjSync<Column extends keyof T>(key: string, column: Column): {
94
+ key: string;
95
+ value: T[Column];
96
+ time: number;
97
+ } | undefined;
87
98
  getColumnSync<Column extends keyof T>(column: Column): {
88
99
  key: string;
89
100
  value: T[Column];
101
+ time: number;
90
102
  }[] | undefined;
91
103
  getColumnInfo(): Promise<{
92
104
  column: string;
@@ -883,45 +883,60 @@ export class BulkDatabaseBase<T extends { key: string }> {
883
883
  }
884
884
 
885
885
  // Applies the overlay (pending writes/deletes) on top of a base column. No-op when empty. An
886
- // overlay entry that doesn't include this column leaves the base (disk) value in place — a partial
887
- // write/update only overrides the columns it set; everything else falls through.
888
- private patchColumn(base: { key: string; value: unknown }[], column: string): { key: string; value: unknown }[] {
886
+ // overlay entry that doesn't include this column leaves the base (disk) value+time in place — a
887
+ // partial write/update only overrides the columns it set; everything else falls through. An overlay
888
+ // override carries the overlay write's time (when that pending write happened).
889
+ private patchColumn(base: { key: string; value: unknown; time: number }[], column: string): { key: string; value: unknown; time: number }[] {
889
890
  if (this.overlay.size === 0) return base;
890
- const map = new Map(base.map(e => [e.key, e.value]));
891
+ const map = new Map(base.map(e => [e.key, { value: e.value, time: e.time }]));
891
892
  for (const [key, entry] of this.overlay) {
892
893
  if (entry.value === DELETED) { map.delete(key); continue; }
893
- if (column in entry.value) map.set(key, entry.value[column]);
894
- else if (!map.has(key)) map.set(key, undefined);
894
+ if (column in entry.value) map.set(key, { value: entry.value[column], time: entry.time });
895
+ else if (!map.has(key)) map.set(key, { value: undefined, time: entry.time });
895
896
  }
896
- return [...map].map(([key, value]) => ({ key, value }));
897
+ return [...map].map(([key, v]) => ({ key, value: v.value, time: v.time }));
897
898
  }
898
899
 
899
900
  // ---- async reads (overlay-aware) ----
900
901
 
901
902
  public async getSingleField<Column extends keyof T>(key: string, column: Column): Promise<T[Column] | undefined> {
903
+ return (await this.getSingleFieldObj(key, column))?.value;
904
+ }
905
+
906
+ // Like getSingleField, but returns the same shape a getColumn entry has: { key, value, time }, where
907
+ // time is roughly when that value last changed (the resolved write-time; for a row-merged value it's
908
+ // the newest contributing write). Returns undefined only when the key isn't present/live.
909
+ public async getSingleFieldObj<Column extends keyof T>(key: string, column: Column): Promise<{ key: string; value: T[Column]; time: number } | undefined> {
902
910
  void this.syncSetup();
911
+ const col = String(column);
903
912
  const entry = this.overlay.get(key);
904
913
  if (entry !== undefined) {
905
914
  if (entry.value === DELETED) return undefined;
906
- if (String(column) in entry.value) return entry.value[String(column)] as T[Column];
907
- // column not set in the overlay entry — fall through to disk
915
+ if (col in entry.value) return { key, value: entry.value[col] as T[Column], time: entry.time };
916
+ // column not set in the overlay entry — fall through to disk for this column
908
917
  }
909
918
  let time = Date.now();
910
919
  let reader = await this.reader();
911
- let result = await reader.getSingleField(key, String(column)) as T[Column] | undefined;
920
+ const r = await reader.getSingleField(key, col);
912
921
  time = Date.now() - time;
913
922
  if (time > 50) {
914
- console.log(`${blue(`${this.name}.getSingleField(${JSON.stringify(key)}, ${JSON.stringify(column)})`)} took ${red(formatTime(time))} ${this.formatInfo(reader)}`);
923
+ console.log(`${blue(`${this.name}.getSingleFieldObj(${JSON.stringify(key)}, ${JSON.stringify(column)})`)} took ${red(formatTime(time))} ${this.formatInfo(reader)}`);
915
924
  }
916
- return result;
925
+ if (r === undefined) {
926
+ // Not live on disk; but if the overlay holds the key (a partial write of a not-yet-on-disk
927
+ // key) it's live with this column unset.
928
+ if (entry !== undefined && entry.value !== DELETED) return { key, value: undefined as T[Column], time: entry.time };
929
+ return undefined;
930
+ }
931
+ return { key, value: r.value as T[Column], time: r.time };
917
932
  }
918
933
 
919
- public async getColumn<Column extends keyof T>(column: Column): Promise<{ key: string; value: T[Column] }[]> {
934
+ public async getColumn<Column extends keyof T>(column: Column): Promise<{ key: string; value: T[Column]; time: number }[]> {
920
935
  void this.syncSetup();
921
936
  let time = Date.now();
922
937
  let reader = await this.reader();
923
- let base = await reader.getColumn(String(column)) as { key: string; value: T[Column] }[];
924
- let result = this.patchColumn(base, String(column)) as { key: string; value: T[Column] }[];
938
+ let base = await reader.getColumn(String(column));
939
+ let result = this.patchColumn(base, String(column)) as { key: string; value: T[Column]; time: number }[];
925
940
  time = Date.now() - time;
926
941
  if (time > 50) {
927
942
  console.log(`${blue(`${this.name}.getColumn(${JSON.stringify(column)})`)} took ${red(formatTime(time))} ${this.formatInfo(reader)}`);
@@ -947,9 +962,11 @@ export class BulkDatabaseBase<T extends { key: string }> {
947
962
  // loaded once and cached; the overlay is layered on top (we can't async-cache the combined result
948
963
  // because the overlay mutates).
949
964
 
950
- private baseColumns = new Map<string, { key: string; value: unknown }[]>();
965
+ private baseColumns = new Map<string, { key: string; value: unknown; time: number }[]>();
951
966
  private baseColumnsLoading = new Set<string>();
952
- private baseFields = new Map<string, unknown>();
967
+ // The disk-resolved field: { value, time } when the key is live on disk, or undefined when it isn't
968
+ // (Map.has distinguishes "loaded" from "not loaded yet").
969
+ private baseFields = new Map<string, { value: unknown; time: number } | undefined>();
953
970
  private baseFieldsLoading = new Set<string>();
954
971
 
955
972
  private ensureBaseColumn(column: string) {
@@ -972,9 +989,9 @@ export class BulkDatabaseBase<T extends { key: string }> {
972
989
  this.baseFieldsLoading.add(cacheKey);
973
990
  void (async () => {
974
991
  let reader = await this.reader();
975
- let value = await reader.getSingleField(key, column);
992
+ let resolved = await reader.getSingleField(key, column);
976
993
  this.deps.batch(() => {
977
- this.baseFields.set(cacheKey, value);
994
+ this.baseFields.set(cacheKey, resolved);
978
995
  this.baseFieldsLoading.delete(cacheKey);
979
996
  this.deps.invalidate(LOAD_SIGNAL);
980
997
  });
@@ -982,6 +999,12 @@ export class BulkDatabaseBase<T extends { key: string }> {
982
999
  }
983
1000
 
984
1001
  public getSingleFieldSync<Column extends keyof T>(key: string, column: Column): T[Column] | undefined {
1002
+ return this.getSingleFieldObjSync(key, column)?.value;
1003
+ }
1004
+
1005
+ // Sync (reactive) counterpart of getSingleFieldObj: { key, value, time } once loaded, undefined while
1006
+ // loading or when the key isn't present/live. time is roughly when the value last changed.
1007
+ public getSingleFieldObjSync<Column extends keyof T>(key: string, column: Column): { key: string; value: T[Column]; time: number } | undefined {
985
1008
  void this.syncSetup();
986
1009
  this.deps.observe(LOAD_SIGNAL);
987
1010
  this.deps.observe(key);
@@ -989,18 +1012,24 @@ export class BulkDatabaseBase<T extends { key: string }> {
989
1012
  let entry = this.overlay.get(key);
990
1013
  if (entry !== undefined) {
991
1014
  if (entry.value === DELETED) return undefined;
992
- if (col in entry.value) return entry.value[col] as T[Column];
993
- // column not set in the overlay entry — fall through to the base field cache
1015
+ if (col in entry.value) return { key, value: entry.value[col] as T[Column], time: entry.time };
1016
+ // column not set in the overlay entry — fall through to the base field cache for this column
994
1017
  }
995
1018
  let cacheKey = nullJoin(col, key);
996
1019
  if (!this.baseFields.has(cacheKey)) {
997
1020
  this.ensureBaseField(key, col);
998
1021
  return undefined;
999
1022
  }
1000
- return this.baseFields.get(cacheKey) as T[Column] | undefined;
1023
+ const base = this.baseFields.get(cacheKey);
1024
+ if (base === undefined) {
1025
+ // Not live on disk; but an overlay entry for the key (partial write) makes it live, column unset.
1026
+ if (entry !== undefined && entry.value !== DELETED) return { key, value: undefined as T[Column], time: entry.time };
1027
+ return undefined;
1028
+ }
1029
+ return { key, value: base.value as T[Column], time: base.time };
1001
1030
  }
1002
1031
 
1003
- public getColumnSync<Column extends keyof T>(column: Column): { key: string; value: T[Column] }[] | undefined {
1032
+ public getColumnSync<Column extends keyof T>(column: Column): { key: string; value: T[Column]; time: number }[] | undefined {
1004
1033
  void this.syncSetup();
1005
1034
  this.deps.observe(LOAD_SIGNAL);
1006
1035
  // Observe the overlay-wide signal so we recompute once the base arrives or the overlay changes.
@@ -1011,7 +1040,7 @@ export class BulkDatabaseBase<T extends { key: string }> {
1011
1040
  this.ensureBaseColumn(col);
1012
1041
  return undefined;
1013
1042
  }
1014
- return this.patchColumn(base, col) as { key: string; value: T[Column] }[];
1043
+ return this.patchColumn(base, col) as { key: string; value: T[Column]; time: number }[];
1015
1044
  }
1016
1045
 
1017
1046
  public async getColumnInfo() {
@@ -1031,15 +1060,17 @@ export class BulkDatabaseBase<T extends { key: string }> {
1031
1060
  }
1032
1061
  }
1033
1062
 
1034
- // The merged, time-resolved view over all readers. getColumn/getSingleField return plain resolved
1035
- // values (not {value,time}); the base layers the overlay on top of these.
1063
+ // The merged, time-resolved view over all readers. getColumn/getSingleField return the resolved value
1064
+ // AND its write-time (so reads can expose roughly when a value last changed); the base layers the
1065
+ // overlay on top of these. getSingleField returns undefined only when the key isn't live (deleted or
1066
+ // absent) — a live key whose column is merely unset returns { value: undefined, time: rowTime }.
1036
1067
  type ResolvedReader = {
1037
1068
  rowCount: number;
1038
1069
  totalBytes: number;
1039
1070
  keys: string[];
1040
1071
  columns: { column: string; byteSize: number }[];
1041
- getColumn: (column: string) => Promise<{ key: string; value: unknown }[]>;
1042
- getSingleField: (key: string, column: string) => Promise<unknown | undefined>;
1072
+ getColumn: (column: string) => Promise<{ key: string; value: unknown; time: number }[]>;
1073
+ getSingleField: (key: string, column: string) => Promise<{ value: unknown; time: number } | undefined>;
1043
1074
  };
1044
1075
 
1045
1076
  // Resolve every read by ACTUAL write-time across all readers (stream + bulk), per key and per column:
@@ -1097,10 +1128,15 @@ async function joinBulkDatabases(databases: BaseBulkDatabaseReader[]): Promise<R
1097
1128
  if (!cell || cell.value === ABSENT) continue;
1098
1129
  if (cell.time > bestTime) { bestTime = cell.time; bestVal = cell.value; found = true; }
1099
1130
  }
1100
- return { key, value: (found && bestTime > delOf(key)) ? bestVal : undefined };
1131
+ // time = the column value's write-time when this column has one, else the row's last
1132
+ // write-time (the key is live but never set this column).
1133
+ const live = found && bestTime > delOf(key);
1134
+ return { key, value: live ? bestVal : undefined, time: live ? bestTime : (keyTime.get(key) ?? 0) };
1101
1135
  });
1102
1136
  },
1103
1137
  async getSingleField(key, column) {
1138
+ const kt = keyTime.get(key);
1139
+ if (kt === undefined || kt <= delOf(key)) return undefined; // key not live
1104
1140
  let bestTime = -Infinity;
1105
1141
  let bestVal: unknown;
1106
1142
  let found = false;
@@ -1110,7 +1146,8 @@ async function joinBulkDatabases(databases: BaseBulkDatabaseReader[]): Promise<R
1110
1146
  if (r === ABSENT) continue;
1111
1147
  if (r.time > bestTime) { bestTime = r.time; bestVal = r.value; found = true; }
1112
1148
  }
1113
- return (found && bestTime > delOf(key)) ? bestVal : undefined;
1149
+ const live = found && bestTime > delOf(key);
1150
+ return { value: live ? bestVal : undefined, time: live ? bestTime : kt };
1114
1151
  },
1115
1152
  };
1116
1153
  }
@@ -396,12 +396,15 @@ export const getFileStorageNested2 = cache(async function getFileStorage(pathStr
396
396
  } else {
397
397
  base = await getDirectoryHandle();
398
398
  let dirs: string[] = [];
399
+ let alls: string[] = [];
399
400
  for await (const [name, entry] of base) {
400
401
  if (entry.kind === "directory") {
401
402
  dirs.push(name);
402
403
  }
404
+ alls.push(name);
403
405
  }
404
- if (dirs.includes(".git") || dirs.includes("data")) {
406
+ // HACK: If there are enough files, it's almost certainly not a directory that contains collections. It's probably the user's data instead
407
+ if (dirs.includes(".git") || dirs.includes("data") || alls.length > 100) {
405
408
  base = await base.getDirectoryHandle("data", { create: true });
406
409
  }
407
410
  }