@firtoz/idb-collections 0.2.2 → 0.3.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.
- package/README.md +46 -0
- package/dist/chunk-NGMCD6BY.js +86 -0
- package/dist/chunk-NGMCD6BY.js.map +1 -0
- package/dist/chunk-RKCR4KWV.js +177 -0
- package/dist/chunk-RKCR4KWV.js.map +1 -0
- package/dist/idb-query-utils.d.ts +30 -0
- package/dist/idb-query-utils.js +3 -0
- package/dist/idb-query-utils.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/keyvalCollection.d.ts +40 -0
- package/dist/keyvalCollection.js +3 -0
- package/dist/keyvalCollection.js.map +1 -0
- package/package.json +15 -10
package/README.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# @firtoz/idb-collections
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@firtoz/idb-collections)
|
|
4
|
+
[](https://www.npmjs.com/package/@firtoz/idb-collections)
|
|
5
|
+
[](https://github.com/firtoz/fullstack-toolkit/blob/main/LICENSE)
|
|
6
|
+
|
|
7
|
+
[](https://www.typescriptlang.org/)
|
|
8
|
+
[](https://tanstack.com/db)
|
|
9
|
+
[](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API)
|
|
10
|
+
|
|
11
|
+
**Key-value collections on IndexedDB for [TanStack DB](https://tanstack.com/db)**—with a typed adapter, helpers to build key ranges from queries, and a path to fast local reads in the browser.
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install @firtoz/idb-collections @tanstack/db @standard-schema/spec
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Also: `pnpm add` · `bun add` · `yarn add`
|
|
20
|
+
|
|
21
|
+
Peer dependencies: `@tanstack/db` and `@standard-schema/spec`. The package builds on [`@firtoz/db-helpers`](https://www.npmjs.com/package/@firtoz/db-helpers) for shared collection utilities.
|
|
22
|
+
|
|
23
|
+
## What you get
|
|
24
|
+
|
|
25
|
+
- **`createKeyValCollection` / `keyvalCollectionOptions`** — configure a TanStack DB collection backed by IndexedDB key-value storage.
|
|
26
|
+
- **`tryExtractIndexedQuery`**, **`KeyRangeSpec`** — translate query intent into IDB key ranges where possible.
|
|
27
|
+
|
|
28
|
+
## Usage
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
import {
|
|
32
|
+
createKeyValCollection,
|
|
33
|
+
keyvalCollectionOptions,
|
|
34
|
+
tryExtractIndexedQuery,
|
|
35
|
+
} from "@firtoz/idb-collections";
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
See tests and consumers in the monorepo for full patterns.
|
|
39
|
+
|
|
40
|
+
## Links
|
|
41
|
+
|
|
42
|
+
- [GitHub](https://github.com/firtoz/fullstack-toolkit/tree/main/packages/idb-collections) · [Issues](https://github.com/firtoz/fullstack-toolkit/issues)
|
|
43
|
+
|
|
44
|
+
## License
|
|
45
|
+
|
|
46
|
+
MIT © [Firtina Ozbalikchi](https://github.com/firtoz)
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { extractSimpleComparisons } from '@tanstack/db';
|
|
2
|
+
|
|
3
|
+
// src/idb-query-utils.ts
|
|
4
|
+
function tryExtractIndexedQuery(expression, indexes, debug) {
|
|
5
|
+
if (!indexes) {
|
|
6
|
+
return null;
|
|
7
|
+
}
|
|
8
|
+
try {
|
|
9
|
+
let comparisons;
|
|
10
|
+
try {
|
|
11
|
+
comparisons = extractSimpleComparisons(expression);
|
|
12
|
+
} catch {
|
|
13
|
+
if (debug) {
|
|
14
|
+
console.warn(
|
|
15
|
+
"Indexed query extraction skipped: expression not supported by extractSimpleComparisons"
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
if (comparisons.length !== 1) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
const comparison = comparisons[0];
|
|
24
|
+
const fieldName = comparison.field.join(".");
|
|
25
|
+
const lastIdx = comparison.field.length - 1;
|
|
26
|
+
const lastSegment = lastIdx >= 0 ? comparison.field[lastIdx] ?? "" : "";
|
|
27
|
+
const indexName = indexes[fieldName] ?? indexes[lastSegment];
|
|
28
|
+
if (!indexName) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
let keyRange = null;
|
|
32
|
+
switch (comparison.operator) {
|
|
33
|
+
case "eq":
|
|
34
|
+
keyRange = { type: "only", value: comparison.value };
|
|
35
|
+
break;
|
|
36
|
+
case "gt":
|
|
37
|
+
keyRange = {
|
|
38
|
+
type: "lowerBound",
|
|
39
|
+
lower: comparison.value,
|
|
40
|
+
lowerOpen: true
|
|
41
|
+
};
|
|
42
|
+
break;
|
|
43
|
+
case "gte":
|
|
44
|
+
keyRange = {
|
|
45
|
+
type: "lowerBound",
|
|
46
|
+
lower: comparison.value,
|
|
47
|
+
lowerOpen: false
|
|
48
|
+
};
|
|
49
|
+
break;
|
|
50
|
+
case "lt":
|
|
51
|
+
keyRange = {
|
|
52
|
+
type: "upperBound",
|
|
53
|
+
upper: comparison.value,
|
|
54
|
+
upperOpen: true
|
|
55
|
+
};
|
|
56
|
+
break;
|
|
57
|
+
case "lte":
|
|
58
|
+
keyRange = {
|
|
59
|
+
type: "upperBound",
|
|
60
|
+
upper: comparison.value,
|
|
61
|
+
upperOpen: false
|
|
62
|
+
};
|
|
63
|
+
break;
|
|
64
|
+
default:
|
|
65
|
+
if (debug) {
|
|
66
|
+
console.warn(
|
|
67
|
+
`Skipping indexed query extraction for unsupported operator: ${comparison.operator}`
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
if (!keyRange) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
return { fieldName, indexName, keyRange };
|
|
76
|
+
} catch (error) {
|
|
77
|
+
if (debug) {
|
|
78
|
+
console.warn("Error extracting indexed query", error, expression);
|
|
79
|
+
}
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export { tryExtractIndexedQuery };
|
|
85
|
+
//# sourceMappingURL=chunk-NGMCD6BY.js.map
|
|
86
|
+
//# sourceMappingURL=chunk-NGMCD6BY.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/idb-query-utils.ts"],"names":[],"mappings":";;;AAyBO,SAAS,sBAAA,CACf,UAAA,EACA,OAAA,EACA,KAAA,EAC0E;AAC1E,EAAA,IAAI,CAAC,OAAA,EAAS;AACb,IAAA,OAAO,IAAA;AAAA,EACR;AAEA,EAAA,IAAI;AACH,IAAA,IAAI,WAAA;AACJ,IAAA,IAAI;AACH,MAAA,WAAA,GAAc,yBAAyB,UAAU,CAAA;AAAA,IAClD,CAAA,CAAA,MAAQ;AAEP,MAAA,IAAI,KAAA,EAAO;AACV,QAAA,OAAA,CAAQ,IAAA;AAAA,UACP;AAAA,SACD;AAAA,MACD;AACA,MAAA,OAAO,IAAA;AAAA,IACR;AAEA,IAAA,IAAI,WAAA,CAAY,WAAW,CAAA,EAAG;AAC7B,MAAA,OAAO,IAAA;AAAA,IACR;AAEA,IAAA,MAAM,UAAA,GAAa,YAAY,CAAC,CAAA;AAChC,IAAA,MAAM,SAAA,GAAY,UAAA,CAAW,KAAA,CAAM,IAAA,CAAK,GAAG,CAAA;AAC3C,IAAA,MAAM,OAAA,GAAU,UAAA,CAAW,KAAA,CAAM,MAAA,GAAS,CAAA;AAC1C,IAAA,MAAM,cAAc,OAAA,IAAW,CAAA,GAAK,WAAW,KAAA,CAAM,OAAO,KAAK,EAAA,GAAM,EAAA;AAEvE,IAAA,MAAM,SAAA,GAAY,OAAA,CAAQ,SAAS,CAAA,IAAK,QAAQ,WAAW,CAAA;AAE3D,IAAA,IAAI,CAAC,SAAA,EAAW;AACf,MAAA,OAAO,IAAA;AAAA,IACR;AAEA,IAAA,IAAI,QAAA,GAAgC,IAAA;AAEpC,IAAA,QAAQ,WAAW,QAAA;AAAU,MAC5B,KAAK,IAAA;AACJ,QAAA,QAAA,GAAW,EAAE,IAAA,EAAM,MAAA,EAAQ,KAAA,EAAO,WAAW,KAAA,EAAM;AACnD,QAAA;AAAA,MACD,KAAK,IAAA;AACJ,QAAA,QAAA,GAAW;AAAA,UACV,IAAA,EAAM,YAAA;AAAA,UACN,OAAO,UAAA,CAAW,KAAA;AAAA,UAClB,SAAA,EAAW;AAAA,SACZ;AACA,QAAA;AAAA,MACD,KAAK,KAAA;AACJ,QAAA,QAAA,GAAW;AAAA,UACV,IAAA,EAAM,YAAA;AAAA,UACN,OAAO,UAAA,CAAW,KAAA;AAAA,UAClB,SAAA,EAAW;AAAA,SACZ;AACA,QAAA;AAAA,MACD,KAAK,IAAA;AACJ,QAAA,QAAA,GAAW;AAAA,UACV,IAAA,EAAM,YAAA;AAAA,UACN,OAAO,UAAA,CAAW,KAAA;AAAA,UAClB,SAAA,EAAW;AAAA,SACZ;AACA,QAAA;AAAA,MACD,KAAK,KAAA;AACJ,QAAA,QAAA,GAAW;AAAA,UACV,IAAA,EAAM,YAAA;AAAA,UACN,OAAO,UAAA,CAAW,KAAA;AAAA,UAClB,SAAA,EAAW;AAAA,SACZ;AACA,QAAA;AAAA,MACD;AACC,QAAA,IAAI,KAAA,EAAO;AACV,UAAA,OAAA,CAAQ,IAAA;AAAA,YACP,CAAA,4DAAA,EAA+D,WAAW,QAAQ,CAAA;AAAA,WACnF;AAAA,QACD;AACA,QAAA,OAAO,IAAA;AAAA;AAGT,IAAA,IAAI,CAAC,QAAA,EAAU;AACd,MAAA,OAAO,IAAA;AAAA,IACR;AAEA,IAAA,OAAO,EAAE,SAAA,EAAW,SAAA,EAAW,QAAA,EAAS;AAAA,EACzC,SAAS,KAAA,EAAO;AACf,IAAA,IAAI,KAAA,EAAO;AACV,MAAA,OAAA,CAAQ,IAAA,CAAK,gCAAA,EAAkC,KAAA,EAAO,UAAU,CAAA;AAAA,IACjE;AACA,IAAA,OAAO,IAAA;AAAA,EACR;AACD","file":"chunk-NGMCD6BY.js","sourcesContent":["import type { IR } from \"@tanstack/db\";\nimport { extractSimpleComparisons } from \"@tanstack/db\";\n\n/**\n * Key range specification for index queries.\n * Used by IndexedDB implementations to build IDBKeyRange objects.\n */\nexport interface KeyRangeSpec {\n\ttype: \"only\" | \"lowerBound\" | \"upperBound\" | \"bound\";\n\tvalue?: unknown;\n\tlower?: unknown;\n\tupper?: unknown;\n\tlowerOpen?: boolean;\n\tupperOpen?: boolean;\n}\n\n/**\n * Attempts to extract a simple indexed query from an IR expression.\n * Returns the field name and key range if the query can be optimized.\n *\n * IndexedDB indexes are much more limited than SQL WHERE clauses:\n * - Only supports simple comparisons on a SINGLE indexed field\n * - Supported operators: eq, gt, gte, lt, lte\n * - Complex queries (AND, OR, NOT, multiple fields) fall back to in-memory filtering\n */\nexport function tryExtractIndexedQuery(\n\texpression: IR.BasicExpression,\n\tindexes?: Record<string, string>,\n\tdebug?: boolean,\n): { fieldName: string; indexName: string; keyRange: KeyRangeSpec } | null {\n\tif (!indexes) {\n\t\treturn null;\n\t}\n\n\ttry {\n\t\tlet comparisons: ReturnType<typeof extractSimpleComparisons>;\n\t\ttry {\n\t\t\tcomparisons = extractSimpleComparisons(expression);\n\t\t} catch {\n\t\t\t// e.g. `like` and other ops TanStack does not decompose here — fall back to full scan + filter\n\t\t\tif (debug) {\n\t\t\t\tconsole.warn(\n\t\t\t\t\t\"Indexed query extraction skipped: expression not supported by extractSimpleComparisons\",\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn null;\n\t\t}\n\n\t\tif (comparisons.length !== 1) {\n\t\t\treturn null;\n\t\t}\n\n\t\tconst comparison = comparisons[0];\n\t\tconst fieldName = comparison.field.join(\".\");\n\t\tconst lastIdx = comparison.field.length - 1;\n\t\tconst lastSegment = lastIdx >= 0 ? (comparison.field[lastIdx] ?? \"\") : \"\";\n\t\t// TanStack may use nested refs (`todo.priority`); IDB indexes are keyed by column (e.g. `priority`)\n\t\tconst indexName = indexes[fieldName] ?? indexes[lastSegment];\n\n\t\tif (!indexName) {\n\t\t\treturn null;\n\t\t}\n\n\t\tlet keyRange: KeyRangeSpec | null = null;\n\n\t\tswitch (comparison.operator) {\n\t\t\tcase \"eq\":\n\t\t\t\tkeyRange = { type: \"only\", value: comparison.value };\n\t\t\t\tbreak;\n\t\t\tcase \"gt\":\n\t\t\t\tkeyRange = {\n\t\t\t\t\ttype: \"lowerBound\",\n\t\t\t\t\tlower: comparison.value,\n\t\t\t\t\tlowerOpen: true,\n\t\t\t\t};\n\t\t\t\tbreak;\n\t\t\tcase \"gte\":\n\t\t\t\tkeyRange = {\n\t\t\t\t\ttype: \"lowerBound\",\n\t\t\t\t\tlower: comparison.value,\n\t\t\t\t\tlowerOpen: false,\n\t\t\t\t};\n\t\t\t\tbreak;\n\t\t\tcase \"lt\":\n\t\t\t\tkeyRange = {\n\t\t\t\t\ttype: \"upperBound\",\n\t\t\t\t\tupper: comparison.value,\n\t\t\t\t\tupperOpen: true,\n\t\t\t\t};\n\t\t\t\tbreak;\n\t\t\tcase \"lte\":\n\t\t\t\tkeyRange = {\n\t\t\t\t\ttype: \"upperBound\",\n\t\t\t\t\tupper: comparison.value,\n\t\t\t\t\tupperOpen: false,\n\t\t\t\t};\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\tif (debug) {\n\t\t\t\t\tconsole.warn(\n\t\t\t\t\t\t`Skipping indexed query extraction for unsupported operator: ${comparison.operator}`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\treturn null;\n\t\t}\n\n\t\tif (!keyRange) {\n\t\t\treturn null;\n\t\t}\n\n\t\treturn { fieldName, indexName, keyRange };\n\t} catch (error) {\n\t\tif (debug) {\n\t\t\tconsole.warn(\"Error extracting indexed query\", error, expression);\n\t\t}\n\t\treturn null;\n\t}\n}\n"]}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { createCollection, parseOrderByExpression } from '@tanstack/db';
|
|
2
|
+
import { createGenericSyncFunction, createGenericCollectionConfig, evaluateExpression } from '@firtoz/db-helpers';
|
|
3
|
+
|
|
4
|
+
// src/keyvalCollection.ts
|
|
5
|
+
function defaultGetKey(item) {
|
|
6
|
+
return item.id;
|
|
7
|
+
}
|
|
8
|
+
function keyvalCollectionOptions(config) {
|
|
9
|
+
const adapter = config.adapter;
|
|
10
|
+
const getKey = config.getKey ?? defaultGetKey;
|
|
11
|
+
const readyPromise = config.readyPromise ?? Promise.resolve();
|
|
12
|
+
const adapterSetMany = async (entries) => {
|
|
13
|
+
if (adapter.setMany) {
|
|
14
|
+
await adapter.setMany(entries);
|
|
15
|
+
} else {
|
|
16
|
+
for (const [key, value] of entries) {
|
|
17
|
+
await adapter.set(key, value);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
const adapterDelMany = async (keys) => {
|
|
22
|
+
if (adapter.delMany) {
|
|
23
|
+
await adapter.delMany(keys);
|
|
24
|
+
} else {
|
|
25
|
+
for (const key of keys) {
|
|
26
|
+
await adapter.del(key);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
const backend = {
|
|
31
|
+
initialLoad: async () => {
|
|
32
|
+
const allEntries = await adapter.entries();
|
|
33
|
+
return allEntries.map(([, value]) => value);
|
|
34
|
+
},
|
|
35
|
+
loadSubset: async (options) => {
|
|
36
|
+
const allEntries = await adapter.entries();
|
|
37
|
+
let items = allEntries.map(([, value]) => value);
|
|
38
|
+
let combinedWhere = options.where;
|
|
39
|
+
if (options.cursor?.whereFrom) {
|
|
40
|
+
if (combinedWhere) {
|
|
41
|
+
combinedWhere = {
|
|
42
|
+
type: "func",
|
|
43
|
+
name: "and",
|
|
44
|
+
args: [combinedWhere, options.cursor.whereFrom]
|
|
45
|
+
};
|
|
46
|
+
} else {
|
|
47
|
+
combinedWhere = options.cursor.whereFrom;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (combinedWhere) {
|
|
51
|
+
const whereExpression = combinedWhere;
|
|
52
|
+
items = items.filter(
|
|
53
|
+
(item) => evaluateExpression(whereExpression, item)
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
if (options.orderBy) {
|
|
57
|
+
const sorts = parseOrderByExpression(options.orderBy);
|
|
58
|
+
items.sort((a, b) => {
|
|
59
|
+
for (const sort of sorts) {
|
|
60
|
+
let aValue = a;
|
|
61
|
+
let bValue = b;
|
|
62
|
+
for (const fieldName of sort.field) {
|
|
63
|
+
aValue = aValue?.[fieldName];
|
|
64
|
+
bValue = bValue?.[fieldName];
|
|
65
|
+
}
|
|
66
|
+
if (aValue < bValue) {
|
|
67
|
+
return sort.direction === "asc" ? -1 : 1;
|
|
68
|
+
}
|
|
69
|
+
if (aValue > bValue) {
|
|
70
|
+
return sort.direction === "asc" ? 1 : -1;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return 0;
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
if (options.offset !== void 0 && options.offset > 0) {
|
|
77
|
+
items = items.slice(options.offset);
|
|
78
|
+
}
|
|
79
|
+
if (options.limit !== void 0) {
|
|
80
|
+
items = items.slice(0, options.limit);
|
|
81
|
+
}
|
|
82
|
+
return items;
|
|
83
|
+
},
|
|
84
|
+
handleInsert: async (itemsToInsert) => {
|
|
85
|
+
const entries = itemsToInsert.map((item) => [
|
|
86
|
+
getKey(item),
|
|
87
|
+
item
|
|
88
|
+
]);
|
|
89
|
+
await adapterSetMany(entries);
|
|
90
|
+
return itemsToInsert;
|
|
91
|
+
},
|
|
92
|
+
handleUpdate: async (mutations) => {
|
|
93
|
+
const results = [];
|
|
94
|
+
const entriesToSet = [];
|
|
95
|
+
for (const mutation of mutations) {
|
|
96
|
+
const existing = await adapter.get(mutation.key);
|
|
97
|
+
if (existing) {
|
|
98
|
+
const updatedItem = {
|
|
99
|
+
...existing,
|
|
100
|
+
...mutation.changes
|
|
101
|
+
};
|
|
102
|
+
entriesToSet.push([mutation.key, updatedItem]);
|
|
103
|
+
results.push(updatedItem);
|
|
104
|
+
} else {
|
|
105
|
+
results.push(mutation.original);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (entriesToSet.length > 0) {
|
|
109
|
+
await adapterSetMany(entriesToSet);
|
|
110
|
+
}
|
|
111
|
+
return results;
|
|
112
|
+
},
|
|
113
|
+
handleDelete: async (mutations) => {
|
|
114
|
+
const keysToDelete = mutations.map((m) => m.key);
|
|
115
|
+
await adapterDelMany(keysToDelete);
|
|
116
|
+
},
|
|
117
|
+
handleTruncate: async () => {
|
|
118
|
+
await adapter.clear();
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
const wrappedBackend = {
|
|
122
|
+
...backend,
|
|
123
|
+
initialLoad: async () => {
|
|
124
|
+
if (config.syncMode === "eager" || !config.syncMode) {
|
|
125
|
+
return await backend.initialLoad();
|
|
126
|
+
}
|
|
127
|
+
return [];
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
const baseSyncConfig = {
|
|
131
|
+
readyPromise,
|
|
132
|
+
syncMode: config.syncMode,
|
|
133
|
+
debug: config.debug,
|
|
134
|
+
getSyncPersistKey: getKey
|
|
135
|
+
};
|
|
136
|
+
const syncResult = createGenericSyncFunction(baseSyncConfig, wrappedBackend);
|
|
137
|
+
return createGenericCollectionConfig({
|
|
138
|
+
schema: config.schema,
|
|
139
|
+
getKey,
|
|
140
|
+
syncResult,
|
|
141
|
+
syncMode: config.syncMode,
|
|
142
|
+
onInsert: async (params) => {
|
|
143
|
+
await syncResult.onInsert?.(params);
|
|
144
|
+
const writes = params.transaction.mutations.map((mutation) => ({
|
|
145
|
+
type: "insert",
|
|
146
|
+
value: mutation.modified
|
|
147
|
+
}));
|
|
148
|
+
config.onBroadcast?.(writes);
|
|
149
|
+
},
|
|
150
|
+
onUpdate: async (params) => {
|
|
151
|
+
await syncResult.onUpdate?.(params);
|
|
152
|
+
const writes = params.transaction.mutations.map((mutation) => ({
|
|
153
|
+
type: "update",
|
|
154
|
+
value: mutation.modified,
|
|
155
|
+
previousValue: mutation.original
|
|
156
|
+
}));
|
|
157
|
+
config.onBroadcast?.(writes);
|
|
158
|
+
},
|
|
159
|
+
onDelete: async (params) => {
|
|
160
|
+
await syncResult.onDelete?.(params);
|
|
161
|
+
const writes = params.transaction.mutations.map((mutation) => ({
|
|
162
|
+
type: "delete",
|
|
163
|
+
key: mutation.key
|
|
164
|
+
}));
|
|
165
|
+
config.onBroadcast?.(writes);
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
function createKeyValCollection(config) {
|
|
170
|
+
return createCollection(
|
|
171
|
+
keyvalCollectionOptions(config)
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export { createKeyValCollection, keyvalCollectionOptions };
|
|
176
|
+
//# sourceMappingURL=chunk-RKCR4KWV.js.map
|
|
177
|
+
//# sourceMappingURL=chunk-RKCR4KWV.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/keyvalCollection.ts"],"names":[],"mappings":"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","file":"chunk-RKCR4KWV.js","sourcesContent":["import type { StandardSchemaV1 } from \"@standard-schema/spec\";\nimport {\n\ttype Collection,\n\ttype CollectionConfig,\n\tcreateCollection,\n\ttype InferSchemaInput,\n\ttype InferSchemaOutput,\n\ttype IR,\n\ttype SyncMode,\n\tparseOrderByExpression,\n} from \"@tanstack/db\";\nimport type { SyncMessage, CollectionUtils } from \"@firtoz/db-helpers\";\nimport { evaluateExpression } from \"@firtoz/db-helpers\";\nimport {\n\ttype GenericBaseSyncConfig,\n\ttype GenericSyncBackend,\n\ttype GenericSyncFunctionResult,\n\tcreateGenericSyncFunction,\n\tcreateGenericCollectionConfig,\n} from \"@firtoz/db-helpers\";\n\n/**\n * Minimal key-value storage adapter.\n * Compatible with idb-keyval (almost directly) and localforage (via thin wrapper).\n */\nexport interface KeyValAdapter<T = unknown> {\n\tget(key: string): Promise<T | null | undefined>;\n\tset(key: string, value: T): Promise<void>;\n\tdel(key: string): Promise<void>;\n\tentries(): Promise<[string, T][]>;\n\tclear(): Promise<void>;\n\t/** Optional batch set for performance. Falls back to sequential set() calls. */\n\tsetMany?(entries: [string, T][]): Promise<void>;\n\t/** Optional batch delete for performance. Falls back to sequential del() calls. */\n\tdelMany?(keys: string[]): Promise<void>;\n}\n\nexport interface KeyValCollectionConfig<TSchema extends StandardSchemaV1> {\n\tschema: TSchema;\n\tadapter: KeyValAdapter<InferSchemaOutput<TSchema>>;\n\t/** Extracts the key from an item. Defaults to `(item) => item.id`. */\n\tgetKey?: (item: InferSchemaOutput<TSchema>) => string;\n\t/** Promise that resolves when the adapter is ready. Defaults to resolved. */\n\treadyPromise?: Promise<void>;\n\tsyncMode?: SyncMode;\n\tdebug?: boolean;\n\t/** Called when a local mutation is persisted; use to broadcast to other peers/tabs. */\n\tonBroadcast?: (\n\t\tchanges: SyncMessage<InferSchemaOutput<TSchema>, string | number>[],\n\t) => void;\n}\n\ntype KeyValUtils<TItem> = CollectionUtils<TItem>;\n\nfunction defaultGetKey<TSchema extends StandardSchemaV1>(\n\titem: InferSchemaOutput<TSchema>,\n): string {\n\treturn (item as { id: string }).id;\n}\n\nexport function keyvalCollectionOptions<TSchema extends StandardSchemaV1>(\n\tconfig: KeyValCollectionConfig<TSchema>,\n): CollectionConfig<\n\tInferSchemaOutput<TSchema>,\n\tstring,\n\tTSchema,\n\tKeyValUtils<InferSchemaOutput<TSchema>>\n> & {\n\tutils: KeyValUtils<InferSchemaOutput<TSchema>>;\n\tschema: TSchema;\n} {\n\ttype TItem = InferSchemaOutput<TSchema>;\n\n\tconst adapter = config.adapter;\n\tconst getKey = config.getKey ?? defaultGetKey<TSchema>;\n\tconst readyPromise = config.readyPromise ?? Promise.resolve();\n\n\tconst adapterSetMany = async (entries: [string, TItem][]) => {\n\t\tif (adapter.setMany) {\n\t\t\tawait adapter.setMany(entries);\n\t\t} else {\n\t\t\tfor (const [key, value] of entries) {\n\t\t\t\tawait adapter.set(key, value);\n\t\t\t}\n\t\t}\n\t};\n\n\tconst adapterDelMany = async (keys: string[]) => {\n\t\tif (adapter.delMany) {\n\t\t\tawait adapter.delMany(keys);\n\t\t} else {\n\t\t\tfor (const key of keys) {\n\t\t\t\tawait adapter.del(key);\n\t\t\t}\n\t\t}\n\t};\n\n\tconst backend: GenericSyncBackend<TItem> = {\n\t\tinitialLoad: async () => {\n\t\t\tconst allEntries = await adapter.entries();\n\t\t\treturn allEntries.map(([, value]) => value);\n\t\t},\n\n\t\tloadSubset: async (options) => {\n\t\t\tconst allEntries = await adapter.entries();\n\t\t\tlet items = allEntries.map(([, value]) => value);\n\n\t\t\tlet combinedWhere = options.where;\n\t\t\tif (options.cursor?.whereFrom) {\n\t\t\t\tif (combinedWhere) {\n\t\t\t\t\tcombinedWhere = {\n\t\t\t\t\t\ttype: \"func\",\n\t\t\t\t\t\tname: \"and\",\n\t\t\t\t\t\targs: [combinedWhere, options.cursor.whereFrom],\n\t\t\t\t\t} as IR.Func;\n\t\t\t\t} else {\n\t\t\t\t\tcombinedWhere = options.cursor.whereFrom;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (combinedWhere) {\n\t\t\t\tconst whereExpression = combinedWhere;\n\t\t\t\titems = items.filter((item) =>\n\t\t\t\t\tevaluateExpression(whereExpression, item as Record<string, unknown>),\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tif (options.orderBy) {\n\t\t\t\tconst sorts = parseOrderByExpression(options.orderBy);\n\t\t\t\titems.sort((a, b) => {\n\t\t\t\t\tfor (const sort of sorts) {\n\t\t\t\t\t\t// biome-ignore lint/suspicious/noExplicitAny: Need any for dynamic field access\n\t\t\t\t\t\tlet aValue: any = a;\n\t\t\t\t\t\t// biome-ignore lint/suspicious/noExplicitAny: Need any for dynamic field access\n\t\t\t\t\t\tlet bValue: any = b;\n\t\t\t\t\t\tfor (const fieldName of sort.field) {\n\t\t\t\t\t\t\taValue = aValue?.[fieldName];\n\t\t\t\t\t\t\tbValue = bValue?.[fieldName];\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (aValue < bValue) {\n\t\t\t\t\t\t\treturn sort.direction === \"asc\" ? -1 : 1;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (aValue > bValue) {\n\t\t\t\t\t\t\treturn sort.direction === \"asc\" ? 1 : -1;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\treturn 0;\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tif (options.offset !== undefined && options.offset > 0) {\n\t\t\t\titems = items.slice(options.offset);\n\t\t\t}\n\n\t\t\tif (options.limit !== undefined) {\n\t\t\t\titems = items.slice(0, options.limit);\n\t\t\t}\n\n\t\t\treturn items;\n\t\t},\n\n\t\thandleInsert: async (itemsToInsert) => {\n\t\t\tconst entries: [string, TItem][] = itemsToInsert.map((item) => [\n\t\t\t\tgetKey(item),\n\t\t\t\titem,\n\t\t\t]);\n\t\t\tawait adapterSetMany(entries);\n\t\t\treturn itemsToInsert;\n\t\t},\n\n\t\thandleUpdate: async (mutations) => {\n\t\t\tconst results: TItem[] = [];\n\t\t\tconst entriesToSet: [string, TItem][] = [];\n\n\t\t\tfor (const mutation of mutations) {\n\t\t\t\tconst existing = await adapter.get(mutation.key);\n\t\t\t\tif (existing) {\n\t\t\t\t\tconst updatedItem = {\n\t\t\t\t\t\t...existing,\n\t\t\t\t\t\t...mutation.changes,\n\t\t\t\t\t} as TItem;\n\t\t\t\t\tentriesToSet.push([mutation.key, updatedItem]);\n\t\t\t\t\tresults.push(updatedItem);\n\t\t\t\t} else {\n\t\t\t\t\tresults.push(mutation.original);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (entriesToSet.length > 0) {\n\t\t\t\tawait adapterSetMany(entriesToSet);\n\t\t\t}\n\n\t\t\treturn results;\n\t\t},\n\n\t\thandleDelete: async (mutations) => {\n\t\t\tconst keysToDelete = mutations.map((m) => m.key);\n\t\t\tawait adapterDelMany(keysToDelete);\n\t\t},\n\n\t\thandleTruncate: async () => {\n\t\t\tawait adapter.clear();\n\t\t},\n\t};\n\n\tconst wrappedBackend: GenericSyncBackend<TItem> = {\n\t\t...backend,\n\t\tinitialLoad: async () => {\n\t\t\tif (config.syncMode === \"eager\" || !config.syncMode) {\n\t\t\t\treturn await backend.initialLoad();\n\t\t\t}\n\t\t\treturn [];\n\t\t},\n\t};\n\n\tconst baseSyncConfig: GenericBaseSyncConfig<TItem> = {\n\t\treadyPromise,\n\t\tsyncMode: config.syncMode,\n\t\tdebug: config.debug,\n\t\tgetSyncPersistKey: getKey,\n\t};\n\n\tconst syncResult: GenericSyncFunctionResult<TItem> =\n\t\tcreateGenericSyncFunction(baseSyncConfig, wrappedBackend);\n\n\treturn createGenericCollectionConfig<TItem, TSchema>({\n\t\tschema: config.schema,\n\t\tgetKey,\n\t\tsyncResult,\n\t\tsyncMode: config.syncMode,\n\t\tonInsert: async (params) => {\n\t\t\tawait syncResult.onInsert?.(params);\n\t\t\tconst writes: SyncMessage<TItem, string>[] =\n\t\t\t\tparams.transaction.mutations.map((mutation) => ({\n\t\t\t\t\ttype: \"insert\",\n\t\t\t\t\tvalue: mutation.modified,\n\t\t\t\t}));\n\t\t\tconfig.onBroadcast?.(writes);\n\t\t},\n\t\tonUpdate: async (params) => {\n\t\t\tawait syncResult.onUpdate?.(params);\n\t\t\tconst writes: SyncMessage<TItem, string>[] =\n\t\t\t\tparams.transaction.mutations.map((mutation) => ({\n\t\t\t\t\ttype: \"update\",\n\t\t\t\t\tvalue: mutation.modified,\n\t\t\t\t\tpreviousValue: mutation.original,\n\t\t\t\t}));\n\t\t\tconfig.onBroadcast?.(writes);\n\t\t},\n\t\tonDelete: async (params) => {\n\t\t\tawait syncResult.onDelete?.(params);\n\t\t\tconst writes: SyncMessage<TItem, string>[] =\n\t\t\t\tparams.transaction.mutations.map((mutation) => ({\n\t\t\t\t\ttype: \"delete\",\n\t\t\t\t\tkey: mutation.key,\n\t\t\t\t}));\n\t\t\tconfig.onBroadcast?.(writes);\n\t\t},\n\t});\n}\n\nexport type KeyValCollection<TSchema extends StandardSchemaV1> = Collection<\n\tInferSchemaOutput<TSchema>,\n\tstring,\n\tKeyValUtils<InferSchemaOutput<TSchema>>,\n\tTSchema,\n\tInferSchemaInput<TSchema>\n>;\n\nexport function createKeyValCollection<TSchema extends StandardSchemaV1>(\n\tconfig: KeyValCollectionConfig<TSchema>,\n): KeyValCollection<TSchema> {\n\treturn createCollection(\n\t\tkeyvalCollectionOptions(config),\n\t) as KeyValCollection<TSchema>;\n}\n"]}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { IR } from '@tanstack/db';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Key range specification for index queries.
|
|
5
|
+
* Used by IndexedDB implementations to build IDBKeyRange objects.
|
|
6
|
+
*/
|
|
7
|
+
interface KeyRangeSpec {
|
|
8
|
+
type: "only" | "lowerBound" | "upperBound" | "bound";
|
|
9
|
+
value?: unknown;
|
|
10
|
+
lower?: unknown;
|
|
11
|
+
upper?: unknown;
|
|
12
|
+
lowerOpen?: boolean;
|
|
13
|
+
upperOpen?: boolean;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Attempts to extract a simple indexed query from an IR expression.
|
|
17
|
+
* Returns the field name and key range if the query can be optimized.
|
|
18
|
+
*
|
|
19
|
+
* IndexedDB indexes are much more limited than SQL WHERE clauses:
|
|
20
|
+
* - Only supports simple comparisons on a SINGLE indexed field
|
|
21
|
+
* - Supported operators: eq, gt, gte, lt, lte
|
|
22
|
+
* - Complex queries (AND, OR, NOT, multiple fields) fall back to in-memory filtering
|
|
23
|
+
*/
|
|
24
|
+
declare function tryExtractIndexedQuery(expression: IR.BasicExpression, indexes?: Record<string, string>, debug?: boolean): {
|
|
25
|
+
fieldName: string;
|
|
26
|
+
indexName: string;
|
|
27
|
+
keyRange: KeyRangeSpec;
|
|
28
|
+
} | null;
|
|
29
|
+
|
|
30
|
+
export { type KeyRangeSpec, tryExtractIndexedQuery };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"idb-query-utils.js"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { KeyValAdapter, KeyValCollection, KeyValCollectionConfig, createKeyValCollection, keyvalCollectionOptions } from './keyvalCollection.js';
|
|
2
|
+
export { KeyRangeSpec, tryExtractIndexedQuery } from './idb-query-utils.js';
|
|
3
|
+
import '@standard-schema/spec';
|
|
4
|
+
import '@tanstack/db';
|
|
5
|
+
import '@firtoz/db-helpers';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"index.js"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { StandardSchemaV1 } from '@standard-schema/spec';
|
|
2
|
+
import { InferSchemaOutput, SyncMode, CollectionConfig, Collection, InferSchemaInput } from '@tanstack/db';
|
|
3
|
+
import { SyncMessage, CollectionUtils } from '@firtoz/db-helpers';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Minimal key-value storage adapter.
|
|
7
|
+
* Compatible with idb-keyval (almost directly) and localforage (via thin wrapper).
|
|
8
|
+
*/
|
|
9
|
+
interface KeyValAdapter<T = unknown> {
|
|
10
|
+
get(key: string): Promise<T | null | undefined>;
|
|
11
|
+
set(key: string, value: T): Promise<void>;
|
|
12
|
+
del(key: string): Promise<void>;
|
|
13
|
+
entries(): Promise<[string, T][]>;
|
|
14
|
+
clear(): Promise<void>;
|
|
15
|
+
/** Optional batch set for performance. Falls back to sequential set() calls. */
|
|
16
|
+
setMany?(entries: [string, T][]): Promise<void>;
|
|
17
|
+
/** Optional batch delete for performance. Falls back to sequential del() calls. */
|
|
18
|
+
delMany?(keys: string[]): Promise<void>;
|
|
19
|
+
}
|
|
20
|
+
interface KeyValCollectionConfig<TSchema extends StandardSchemaV1> {
|
|
21
|
+
schema: TSchema;
|
|
22
|
+
adapter: KeyValAdapter<InferSchemaOutput<TSchema>>;
|
|
23
|
+
/** Extracts the key from an item. Defaults to `(item) => item.id`. */
|
|
24
|
+
getKey?: (item: InferSchemaOutput<TSchema>) => string;
|
|
25
|
+
/** Promise that resolves when the adapter is ready. Defaults to resolved. */
|
|
26
|
+
readyPromise?: Promise<void>;
|
|
27
|
+
syncMode?: SyncMode;
|
|
28
|
+
debug?: boolean;
|
|
29
|
+
/** Called when a local mutation is persisted; use to broadcast to other peers/tabs. */
|
|
30
|
+
onBroadcast?: (changes: SyncMessage<InferSchemaOutput<TSchema>, string | number>[]) => void;
|
|
31
|
+
}
|
|
32
|
+
type KeyValUtils<TItem> = CollectionUtils<TItem>;
|
|
33
|
+
declare function keyvalCollectionOptions<TSchema extends StandardSchemaV1>(config: KeyValCollectionConfig<TSchema>): CollectionConfig<InferSchemaOutput<TSchema>, string, TSchema, KeyValUtils<InferSchemaOutput<TSchema>>> & {
|
|
34
|
+
utils: KeyValUtils<InferSchemaOutput<TSchema>>;
|
|
35
|
+
schema: TSchema;
|
|
36
|
+
};
|
|
37
|
+
type KeyValCollection<TSchema extends StandardSchemaV1> = Collection<InferSchemaOutput<TSchema>, string, KeyValUtils<InferSchemaOutput<TSchema>>, TSchema, InferSchemaInput<TSchema>>;
|
|
38
|
+
declare function createKeyValCollection<TSchema extends StandardSchemaV1>(config: KeyValCollectionConfig<TSchema>): KeyValCollection<TSchema>;
|
|
39
|
+
|
|
40
|
+
export { type KeyValAdapter, type KeyValCollection, type KeyValCollectionConfig, createKeyValCollection, keyvalCollectionOptions };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"keyvalCollection.js"}
|
package/package.json
CHANGED
|
@@ -1,23 +1,28 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@firtoz/idb-collections",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "IndexedDB collection utilities for TanStack DB — key-value adapter, query optimization",
|
|
5
|
-
"
|
|
6
|
-
"
|
|
7
|
-
"
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
8
9
|
"exports": {
|
|
9
10
|
".": {
|
|
10
|
-
"types": "./
|
|
11
|
-
"import": "./
|
|
12
|
-
"require": "./src/index.ts"
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js"
|
|
13
13
|
}
|
|
14
14
|
},
|
|
15
15
|
"files": [
|
|
16
|
+
"dist/**/*.js",
|
|
17
|
+
"dist/**/*.js.map",
|
|
18
|
+
"dist/**/*.d.ts",
|
|
16
19
|
"src/**/*.ts",
|
|
17
20
|
"!src/**/*.test.ts",
|
|
18
21
|
"README.md"
|
|
19
22
|
],
|
|
20
23
|
"scripts": {
|
|
24
|
+
"build": "tsup",
|
|
25
|
+
"prepack": "bun run build",
|
|
21
26
|
"typecheck": "tsgo --noEmit -p ./tsconfig.json",
|
|
22
27
|
"test": "bun test",
|
|
23
28
|
"lint": "biome check --write src",
|
|
@@ -51,15 +56,15 @@
|
|
|
51
56
|
},
|
|
52
57
|
"peerDependencies": {
|
|
53
58
|
"@standard-schema/spec": ">=1.1.0",
|
|
54
|
-
"@tanstack/db": ">=0.6.
|
|
59
|
+
"@tanstack/db": ">=0.6.3"
|
|
55
60
|
},
|
|
56
61
|
"devDependencies": {
|
|
57
62
|
"@standard-schema/spec": "^1.1.0",
|
|
58
|
-
"@tanstack/db": "^0.6.
|
|
63
|
+
"@tanstack/db": "^0.6.4",
|
|
59
64
|
"bun-types": "^1.3.11",
|
|
60
65
|
"zod": "^4.3.6"
|
|
61
66
|
},
|
|
62
67
|
"dependencies": {
|
|
63
|
-
"@firtoz/db-helpers": "^2.
|
|
68
|
+
"@firtoz/db-helpers": "^2.2.0"
|
|
64
69
|
}
|
|
65
70
|
}
|