@signaldb/opfs 2.0.0-beta.6 → 2.0.0-beta.7

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.
Files changed (51) hide show
  1. package/dist/base/core/src/AsyncDataAdapter.d.ts +64 -0
  2. package/dist/base/core/src/AutoFetchDataAdapter.d.ts +112 -0
  3. package/dist/base/core/src/Collection/Cursor.d.ts +113 -0
  4. package/dist/base/core/src/Collection/Observer.d.ts +64 -0
  5. package/dist/base/core/src/Collection/index.d.ts +294 -0
  6. package/dist/base/core/src/Collection/types.d.ts +28 -0
  7. package/dist/base/core/src/DataAdapter.d.ts +35 -0
  8. package/dist/base/core/src/DefaultDataAdapter.d.ts +35 -0
  9. package/dist/base/core/src/WorkerDataAdapter.d.ts +25 -0
  10. package/dist/base/core/src/WorkerDataAdapterHost.d.ts +62 -0
  11. package/dist/base/core/src/createIndex.d.ts +7 -0
  12. package/dist/base/core/src/createIndexProvider.d.ts +8 -0
  13. package/dist/base/core/src/createReactivityAdapter.d.ts +8 -0
  14. package/dist/base/core/src/createStorageAdapter.d.ts +9 -0
  15. package/dist/base/core/src/getIndexInfo.d.ts +39 -0
  16. package/dist/base/core/src/index.d.ts +22 -0
  17. package/dist/base/core/src/types/Dependency.d.ts +4 -0
  18. package/dist/base/core/src/types/IndexProvider.d.ts +26 -0
  19. package/dist/base/core/src/types/Modifier.d.ts +46 -0
  20. package/dist/base/core/src/types/ReactivityAdapter.d.ts +6 -0
  21. package/dist/base/core/src/types/Selector.d.ts +46 -0
  22. package/dist/base/core/src/types/Signal.d.ts +4 -0
  23. package/dist/base/core/src/types/StorageAdapter.d.ts +20 -0
  24. package/dist/base/core/src/utils/EventEmitter.d.ts +71 -0
  25. package/dist/base/core/src/utils/batchOnNextTick.d.ts +16 -0
  26. package/dist/base/core/src/utils/compact.d.ts +9 -0
  27. package/dist/base/core/src/utils/createSignal.d.ts +14 -0
  28. package/dist/base/core/src/utils/deepClone.d.ts +17 -0
  29. package/dist/base/core/src/utils/get.d.ts +9 -0
  30. package/dist/base/core/src/utils/getMatchingKeys.d.ts +19 -0
  31. package/dist/base/core/src/utils/intersection.d.ts +9 -0
  32. package/dist/base/core/src/utils/isEqual.d.ts +14 -0
  33. package/dist/base/core/src/utils/isFieldExpression.d.ts +11 -0
  34. package/dist/base/core/src/utils/match.d.ts +12 -0
  35. package/dist/base/core/src/utils/modify.d.ts +14 -0
  36. package/dist/base/core/src/utils/project.d.ts +15 -0
  37. package/dist/base/core/src/utils/queryId.d.ts +9 -0
  38. package/dist/base/core/src/utils/randomId.d.ts +7 -0
  39. package/dist/base/core/src/utils/reactiveOrAsync.d.ts +59 -0
  40. package/dist/base/core/src/utils/serializeValue.d.ts +12 -0
  41. package/dist/base/core/src/utils/set.d.ts +13 -0
  42. package/dist/base/core/src/utils/sortItems.d.ts +12 -0
  43. package/dist/base/core/src/utils/uniqueBy.d.ts +10 -0
  44. package/dist/index.mjs +90 -98
  45. package/dist/index.mjs.map +1 -1
  46. package/dist/index.umd.js +2 -2
  47. package/dist/index.umd.js.map +1 -1
  48. package/dist/storage-adapters/generic-fs/src/deltaHelpers.d.ts +55 -0
  49. package/dist/storage-adapters/generic-fs/src/index.d.ts +31 -0
  50. package/package.json +3 -3
  51. /package/dist/{index.d.ts → storage-adapters/opfs/src/index.d.ts} +0 -0
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Compares two values for deep equality.
3
+ * @param a - The first value to compare.
4
+ * @param b - The second value to compare.
5
+ * @returns - Returns `true` if the two values are deeply equal, otherwise `false`.
6
+ * @example
7
+ * isEqual({ a: 1 }, { a: 1 }); // true
8
+ * isEqual([1, 2], [1, 2]); // true
9
+ * isEqual(new Date(0), new Date(0)); // true
10
+ * isEqual(/abc/, /abc/); // true
11
+ * isEqual({ a: 1 }, { a: 2 }); // false
12
+ * isEqual(null, null); // true
13
+ */
14
+ export default function isEqual<T, K>(a: T, b: K): boolean;
@@ -0,0 +1,11 @@
1
+ import type { FieldExpression } from '../types/Selector';
2
+ /**
3
+ * Determines whether a given object is a valid field expression.
4
+ * A field expression is an object containing query operators supported by MongoDB-style queries.
5
+ * @template T - The type of the field expression.
6
+ * @param expression - The object to test.
7
+ * @returns A boolean indicating whether the object is a valid field expression.
8
+ * - `true` if the object contains only recognized query operators.
9
+ * - `false` otherwise.
10
+ */
11
+ export default function isFieldExpression<T>(expression: any): expression is FieldExpression<T>;
@@ -0,0 +1,12 @@
1
+ import type Selector from '../types/Selector';
2
+ type BaseItem = Record<string, any>;
3
+ /**
4
+ * Tests whether a given item matches a specified selector.
5
+ * Uses the `mingo` library to evaluate the query.
6
+ * @template T - The type of the item being tested.
7
+ * @param item - The item to test against the selector.
8
+ * @param selector - The query selector used to match the item.
9
+ * @returns A boolean indicating whether the item matches the selector.
10
+ */
11
+ export default function match<T extends BaseItem = BaseItem>(item: T, selector: Selector<T>): boolean;
12
+ export {};
@@ -0,0 +1,14 @@
1
+ import type Modifier from '../types/Modifier';
2
+ /**
3
+ * Applies a modifier to an object and returns a new modified object.
4
+ * @template T - The type of the object to be modified.
5
+ * @param item - The object to be modified.
6
+ * @param modifier - The modifier to apply. This can be any transformation logic.
7
+ * @returns - Returns a new object with the modifications applied.
8
+ * @example
9
+ * const item = { a: 1, b: 2 }
10
+ * const modifier = { $set: { b: 3, c: 4 } }
11
+ * const result = modify(item, modifier)
12
+ * // result: { a: 1, b: 3, c: 4 }
13
+ */
14
+ export default function modify<T extends Record<string, any>>(item: T, modifier: Modifier): T;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Projects the fields of an object based on a specified fields configuration.
3
+ * Supports inclusion (`1`) and exclusion (`0`) of specific fields. Creates a new object
4
+ * with the desired fields included or excluded, based on the configuration.
5
+ * @template T - The type of the object being projected.
6
+ * @param item - The original object to project fields from.
7
+ * @param fields - An object defining the fields to include (`1`) or exclude (`0`).
8
+ * - Keys are the field names, and values are either `1` (include) or `0` (exclude).
9
+ * @returns A new object with the specified fields included or excluded.
10
+ * - If all fields are set to `0`, the excluded fields are removed from the result.
11
+ * - If fields are set to `1`, only the included fields are retained.
12
+ */
13
+ export default function project<T extends Record<string, any>>(item: T, fields: {
14
+ [P in keyof T]?: 0 | 1;
15
+ } & Record<string, 0 | 1>): T;
@@ -0,0 +1,9 @@
1
+ import type { QueryOptions } from '../DataAdapter';
2
+ import type Selector from '../types/Selector';
3
+ /**
4
+ * Generates a unique identifier for a query based on its selector and options.
5
+ * @param selector - The selector object.
6
+ * @param options - The query options object (optional).
7
+ * @returns A unique identifier string for the query.
8
+ */
9
+ export default function queryId(selector: Selector<any>, options?: QueryOptions<any>): string;
@@ -0,0 +1,7 @@
1
+ /**
2
+ * creates a random id
3
+ * @returns a random string of 16 characters
4
+ * @example
5
+ * randomId() // '1234567890abcdef'
6
+ */
7
+ export default function randomId(): string;
@@ -0,0 +1,59 @@
1
+ export type MaybePromise<T> = T | Promise<T>;
2
+ /**
3
+ * Options that control execution mode (and potential future mode-specific behavior).
4
+ * Keep this minimal; you can extend it later (e.g. signal, timeoutMs, debugLabel).
5
+ */
6
+ export type ModeOptions = {
7
+ async?: boolean;
8
+ };
9
+ /**
10
+ * A generator helper that makes TypeScript infer the “synchronous value type” for maybe-async expressions.
11
+ *
12
+ * Usage:
13
+ * const doc = yield* unwrap(Collection.findOne(...))
14
+ * const list = yield* unwrap(Collection.find(...).fetch())
15
+ *
16
+ * Runtime note:
17
+ * This does not “unwrap” Promises by itself. It yields the value/Promise to the runner and returns the
18
+ * value that the runner feeds back via `.next(...)`.
19
+ * @param value The value (or Promise of a value) to yield to the runner.
20
+ * @returns A generator that yields `value` and resolves to the runner-supplied unwrapped `T`.
21
+ */
22
+ export declare function unwrap<T>(value: MaybePromise<T>): Generator<MaybePromise<T>, T, T>;
23
+ /**
24
+ * Generator shape used by the factory.
25
+ *
26
+ * `TThis` is the type of `this` inside the generator.
27
+ * `Args` are the method parameters (excluding the mode flag).
28
+ * `TReturn` is the final return value of the workflow.
29
+ * `TNext` is the type that is yielded/awaited and fed back via `.next(...)`.
30
+ *
31
+ * Note:
32
+ * - For best inference at yield sites, prefer `yield* unwrap(expr)` for maybe-async expressions.
33
+ */
34
+ export type ReactiveOrAsyncGen<TThis, Arguments extends any[], TReturn, TNext> = (this: TThis, a: boolean, ...args: Arguments) => Generator<MaybePromise<TNext>, TReturn, TNext>;
35
+ /**
36
+ * The method type produced from the generator signature.
37
+ * Adds overloads so that `{ async: true }` yields a `Promise<...>` return type.
38
+ */
39
+ export type ReactiveOrAsyncMethod<TThis, P extends any[], R, N> = {
40
+ (this: TThis, ...args: P): R;
41
+ (this: TThis, ...args: [...P, ModeOptions?]): MaybePromise<R>;
42
+ (this: TThis, ...args: [...P, {
43
+ async: true;
44
+ }]): Promise<R>;
45
+ } & {
46
+ /** Exposes the underlying generator for composition via `yield* method.generator.call(this, a, ...)` */
47
+ generator: (this: TThis, a: boolean, ...args: P) => Generator<MaybePromise<N>, R, N>;
48
+ };
49
+ /**
50
+ * Factory that turns a generator workflow into a callable method that can run in sync (reactive) or async mode.
51
+ *
52
+ * Call style:
53
+ * fn(a, b) -> sync/reactive return
54
+ * await fn(a, b, { async: true }) -> async return
55
+ * @param gen Generator workflow. Receives `(a)` which indicates async mode and should `yield`/`yield* unwrap(...)`
56
+ * any values that may be Promises.
57
+ * @returns A callable method with overloads plus a `.generator` property for composition.
58
+ */
59
+ export default function reactiveOrAsync<TThis, P extends any[], R, N>(gen: (this: TThis, a: boolean, ...args: P) => Generator<MaybePromise<N>, R, N>): ReactiveOrAsyncMethod<TThis, P, R, N>;
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Serializes a value into a string representation.
3
+ * Handles various types, including strings, numbers, booleans, dates, and objects.
4
+ * Falls back to JSON stringification for unsupported types.
5
+ * @param value - The value to serialize.
6
+ * - Strings are returned as-is.
7
+ * - Numbers and booleans are converted to their string representation.
8
+ * - Dates are converted to ISO string format.
9
+ * - Other values are stringified using `JSON.stringify`.
10
+ * @returns A string representation of the value.
11
+ */
12
+ export default function serializeValue(value: any): string | null;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Sets a value at a specified path within an object. Creates nested structures
3
+ * (arrays or objects) as needed to set the value at the correct location. Supports
4
+ * deleting the key if the value is `undefined` and the `deleteIfUndefined` flag is set to `true`.
5
+ * @template T - The type of the object to modify.
6
+ * @template K - The type of the value to set.
7
+ * @param object - The object to modify. The object is mutated directly.
8
+ * @param path - The path (dot or bracket notation) where the value should be set.
9
+ * @param value - The value to set at the specified path.
10
+ * @param deleteIfUndefined - A boolean indicating whether to delete the key if the value is `undefined` (default: `false`).
11
+ * @returns The modified object.
12
+ */
13
+ export default function set<T extends object, K>(object: T, path: string, value: K, deleteIfUndefined?: boolean): T;
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Sorts an array of items based on multiple fields and their specified sort order.
3
+ * Uses the `fast-sort` library for efficient sorting.
4
+ * @template T - The type of the items in the array.
5
+ * @param items - The array of items to be sorted.
6
+ * @param sortFields - An object defining the sort order for each field.
7
+ * - Keys are the field names, and values are either `1` (ascending) or `-1` (descending).
8
+ * @returns A new array of items sorted based on the specified fields and their order.
9
+ */
10
+ export default function sortItems<T extends Record<string, any>>(items: T[], sortFields: {
11
+ [P in keyof T]?: -1 | 1;
12
+ } & Record<string, -1 | 1>): T[];
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Filters an array to ensure unique values based on a specified key or transformation function.
3
+ * @template T - The type of the elements in the array.
4
+ * @param array - The array to filter for unique values.
5
+ * @param fn - A key or transformation function to determine uniqueness.
6
+ * - If a key is provided, it will use the corresponding property of each element for uniqueness.
7
+ * - If a function is provided, it will use the return value of the function applied to each element for uniqueness.
8
+ * @returns A new array containing only unique elements based on the specified key or transformation.
9
+ */
10
+ export default function uniqueBy<T>(array: T[], fn: keyof T | ((item: T) => any)): T[];
package/dist/index.mjs CHANGED
@@ -1,101 +1,93 @@
1
- import p from "@signaldb/generic-fs";
2
- import { serializeValue as m } from "@signaldb/core";
3
- function u(l) {
4
- let i = l.normalize("NFC");
5
- const d = (s) => s.replaceAll(/[-/\\^$*+?.()|[\]{}]/g, String.raw`\$&`);
6
- return i = i.replaceAll("/", "_"), i = i.replaceAll(new RegExp(`${d("_")}{2,}`, "g"), "_"), i || (i = "unnamed"), i;
1
+ import e from "@signaldb/generic-fs";
2
+ import { serializeValue as t } from "@signaldb/core";
3
+ //#region src/index.ts
4
+ function n(e) {
5
+ let t = e.normalize("NFC");
6
+ return t = t.replaceAll("/", "_"), t = t.replaceAll(RegExp(`${((e) => e.replaceAll(/[-/\\^$*+?.()|[\]{}]/g, String.raw`\$&`))("_")}{2,}`, "g"), "_"), t ||= "unnamed", t;
7
7
  }
8
- async function g(l, y) {
9
- const i = `opfs:${l}`;
10
- return navigator.locks.request(i, { mode: "exclusive" }, y);
8
+ async function r(e, t) {
9
+ let n = `opfs:${e}`;
10
+ return navigator.locks.request(n, { mode: "exclusive" }, t);
11
11
  }
12
- function D(l, y) {
13
- const { serialize: i = JSON.stringify, deserialize: d = JSON.parse } = y || {}, s = async (e, a, o) => {
14
- const r = a.split("/").filter(Boolean);
15
- let t = e;
16
- for (const n of r)
17
- t = await t.getDirectoryHandle(n, { create: o });
18
- return t;
19
- }, w = async (e, a, o) => {
20
- const r = a.split("/").filter(Boolean), t = r.pop();
21
- return (await s(e, r.join("/"), o)).getFileHandle(t, { create: o });
22
- }, f = {
23
- fileNameForId: (e) => Promise.resolve(u(m(e))),
24
- fileNameForIndexKey: (e) => Promise.resolve(u(e)),
25
- joinPath: (...e) => Promise.resolve(e.join("/")),
26
- ensureDir: async (e) => {
27
- const a = await navigator.storage.getDirectory();
28
- await s(a, e, !0);
29
- },
30
- fileExists: async (e) => {
31
- const a = await navigator.storage.getDirectory();
32
- try {
33
- return await w(a, e, !1), !0;
34
- } catch {
35
- const o = e.split("/").filter(Boolean);
36
- let r = a;
37
- for (const t of o)
38
- try {
39
- r = await r.getDirectoryHandle(t);
40
- } catch {
41
- return !1;
42
- }
43
- return !0;
44
- }
45
- },
46
- readObject: async (e) => g(e, async () => {
47
- const a = await navigator.storage.getDirectory(), t = await (await (await w(a, e, !1)).getFile()).text();
48
- return d(t);
49
- }),
50
- writeObject: async (e, a) => g(e, async () => {
51
- const o = await navigator.storage.getDirectory(), r = await w(o, e, !0), t = i(a);
52
- if (typeof t != "string")
53
- throw new TypeError("serialize() must return a string");
54
- const n = await r.createWritable();
55
- try {
56
- const c = new TextEncoder().encode(t);
57
- await n.write({ type: "write", position: 0, data: c }), await n.truncate(c.byteLength), await n.close();
58
- } catch (c) {
59
- throw await n.abort(), c;
60
- }
61
- }),
62
- readIndexObject: async (e) => g(e, async () => {
63
- const a = await navigator.storage.getDirectory(), t = await (await (await w(a, e, !1)).getFile()).text();
64
- return d(t);
65
- }),
66
- writeIndexObject: async (e, a) => g(e, async () => {
67
- const o = await navigator.storage.getDirectory(), r = await w(o, e, !0), t = i(a);
68
- if (typeof t != "string")
69
- throw new TypeError("serialize() must return a string");
70
- const n = await r.createWritable();
71
- try {
72
- const c = new TextEncoder().encode(t);
73
- await n.write({ type: "write", position: 0, data: c }), await n.truncate(c.byteLength), await n.close();
74
- } catch (c) {
75
- throw await n.abort(), c;
76
- }
77
- }),
78
- listFilesRecursive: async (e) => {
79
- const a = await navigator.storage.getDirectory(), o = await s(a, e, !1), r = [];
80
- for await (const t of o.values())
81
- if (t.kind === "file")
82
- r.push(t.name);
83
- else if (t.kind === "directory") {
84
- const n = await f.listFilesRecursive(`${e}/${t.name}`);
85
- r.push(...n.map((c) => `${t.name}/${c}`));
86
- }
87
- return r;
88
- },
89
- removeEntry: async (e, a) => {
90
- const o = await navigator.storage.getDirectory(), r = e.split("/").filter(Boolean), t = r.pop();
91
- if (!t)
92
- throw new Error("Invalid path");
93
- await (r.length > 0 ? await s(o, r.join("/"), !1) : o).removeEntry(t, { recursive: !!a?.recursive });
94
- }
95
- };
96
- return p(f, l);
12
+ function i(i, a) {
13
+ let { serialize: o = JSON.stringify, deserialize: s = JSON.parse } = a || {}, c = async (e, t, n) => {
14
+ let r = t.split("/").filter(Boolean), i = e;
15
+ for (let e of r) i = await i.getDirectoryHandle(e, { create: n });
16
+ return i;
17
+ }, l = async (e, t, n) => {
18
+ let r = t.split("/").filter(Boolean), i = r.pop();
19
+ return (await c(e, r.join("/"), n)).getFileHandle(i, { create: n });
20
+ }, u = {
21
+ fileNameForId: (e) => Promise.resolve(n(t(e))),
22
+ fileNameForIndexKey: (e) => Promise.resolve(n(e)),
23
+ joinPath: (...e) => Promise.resolve(e.join("/")),
24
+ ensureDir: async (e) => {
25
+ await c(await navigator.storage.getDirectory(), e, !0);
26
+ },
27
+ fileExists: async (e) => {
28
+ let t = await navigator.storage.getDirectory();
29
+ try {
30
+ return await l(t, e, !1), !0;
31
+ } catch {
32
+ let n = e.split("/").filter(Boolean), r = t;
33
+ for (let e of n) try {
34
+ r = await r.getDirectoryHandle(e);
35
+ } catch {
36
+ return !1;
37
+ }
38
+ return !0;
39
+ }
40
+ },
41
+ readObject: async (e) => r(e, async () => s(await (await (await l(await navigator.storage.getDirectory(), e, !1)).getFile()).text())),
42
+ writeObject: async (e, t) => r(e, async () => {
43
+ let n = await l(await navigator.storage.getDirectory(), e, !0), r = o(t);
44
+ if (typeof r != "string") throw TypeError("serialize() must return a string");
45
+ let i = await n.createWritable();
46
+ try {
47
+ let e = new TextEncoder().encode(r);
48
+ await i.write({
49
+ type: "write",
50
+ position: 0,
51
+ data: e
52
+ }), await i.truncate(e.byteLength), await i.close();
53
+ } catch (e) {
54
+ throw await i.abort(), e;
55
+ }
56
+ }),
57
+ readIndexObject: async (e) => r(e, async () => s(await (await (await l(await navigator.storage.getDirectory(), e, !1)).getFile()).text())),
58
+ writeIndexObject: async (e, t) => r(e, async () => {
59
+ let n = await l(await navigator.storage.getDirectory(), e, !0), r = o(t);
60
+ if (typeof r != "string") throw TypeError("serialize() must return a string");
61
+ let i = await n.createWritable();
62
+ try {
63
+ let e = new TextEncoder().encode(r);
64
+ await i.write({
65
+ type: "write",
66
+ position: 0,
67
+ data: e
68
+ }), await i.truncate(e.byteLength), await i.close();
69
+ } catch (e) {
70
+ throw await i.abort(), e;
71
+ }
72
+ }),
73
+ listFilesRecursive: async (e) => {
74
+ let t = await c(await navigator.storage.getDirectory(), e, !1), n = [];
75
+ for await (let r of t.values()) if (r.kind === "file") n.push(r.name);
76
+ else if (r.kind === "directory") {
77
+ let t = await u.listFilesRecursive(`${e}/${r.name}`);
78
+ n.push(...t.map((e) => `${r.name}/${e}`));
79
+ }
80
+ return n;
81
+ },
82
+ removeEntry: async (e, t) => {
83
+ let n = await navigator.storage.getDirectory(), r = e.split("/").filter(Boolean), i = r.pop();
84
+ if (!i) throw Error("Invalid path");
85
+ await (r.length > 0 ? await c(n, r.join("/"), !1) : n).removeEntry(i, { recursive: !!t?.recursive });
86
+ }
87
+ };
88
+ return e(u, i);
97
89
  }
98
- export {
99
- D as default
100
- };
101
- //# sourceMappingURL=index.mjs.map
90
+ //#endregion
91
+ export { i as default };
92
+
93
+ //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","sources":["../src/index.ts"],"sourcesContent":["import createGenericFSAdapter from '@signaldb/generic-fs';\nimport { serializeValue } from '@signaldb/core';\n/**\n * Convert an arbitrary filename into a OPFS safe filename.\n * @param input - The input filename to sanitize.\n * @returns A safe filename.\n */\nfunction toSafeFilename(input) {\n const replacement = '_';\n let name = input.normalize('NFC');\n const escapeRegex = (s) => s.replaceAll(/[-/\\\\^$*+?.()|[\\]{}]/g, String.raw `\\$&`);\n name = name.replaceAll('/', replacement);\n name = name.replaceAll(new RegExp(`${escapeRegex(replacement)}{2,}`, 'g'), replacement);\n if (!name)\n name = 'unnamed';\n return name;\n}\n/**\n * Acquire a lock for a specific file path to ensure exclusive access during read/write operations.\n * @param path - The file path to lock.\n * @param fn - The asynchronous function to execute while holding the lock.\n * @returns The result of the function `fn`.\n */\nasync function withPathLock(path, fn) {\n const lockName = `opfs:${path}`;\n // Use exclusive mode so reads cannot interleave with writes\n return navigator.locks.request(lockName, { mode: 'exclusive' }, fn);\n}\n/**\n * Creates a persistence adapter for managing a SignalDB collection using the\n * Origin Private File System (OPFS). This adapter allows data to be stored and managed\n * directly in the browser's file system with support for customizable serialization\n * and deserialization.\n * @template T - The type of the items in the collection.\n * @template I - The type of the unique identifier for the items.\n * @param folderName - The name of the file in OPFS where data will be stored.\n * @param options - Optional configuration for serialization and deserialization.\n * @param options.serialize - A function to serialize items to a string (default: `JSON.stringify`).\n * @param options.deserialize - A function to deserialize a string into items (default: `JSON.parse`).\n * @returns A SignalDB persistence adapter for managing data in OPFS.\n * @example\n * import createOPFSAdapter from './createOPFSAdapter';\n * import { Collection } from '@signaldb/core';\n *\n * const adapter = createOPFSAdapter('myCollection.json', {\n * serialize: (items) => JSON.stringify(items, null, 2), // Pretty-print JSON\n * deserialize: (itemsString) => JSON.parse(itemsString), // Default JSON parse\n * });\n *\n * const collection = new Collection({\n * persistence: adapter,\n * });\n *\n * // Perform operations on the collection, and changes will be reflected in the OPFS file.\n */\nexport default function createOPFSAdapter(folderName, options) {\n const { serialize = JSON.stringify, deserialize = JSON.parse, } = options || {};\n const ensureDirectoryExists = async (rootDirectory, directoryPath, createIfMissing) => {\n const parts = directoryPath.split('/').filter(Boolean);\n let current = rootDirectory;\n for (const part of parts) {\n current = await current.getDirectoryHandle(part, { create: createIfMissing });\n }\n return current;\n };\n const getFileHandleForPath = async (rootDirectory, fullPath, createIfMissing) => {\n const parts = fullPath.split('/').filter(Boolean);\n const fileName = parts.pop();\n const directoryHandle = await ensureDirectoryExists(rootDirectory, parts.join('/'), createIfMissing);\n return directoryHandle.getFileHandle(fileName, { create: createIfMissing });\n };\n const driver = {\n fileNameForId: id => Promise.resolve(toSafeFilename(serializeValue(id))),\n fileNameForIndexKey: key => Promise.resolve(toSafeFilename(key)),\n joinPath: (...parts) => Promise.resolve(parts.join('/')),\n ensureDir: async (directoryPath) => {\n const rootDirectory = await navigator.storage.getDirectory();\n await ensureDirectoryExists(rootDirectory, directoryPath, true);\n },\n fileExists: async (path) => {\n const rootDirectory = await navigator.storage.getDirectory();\n try {\n await getFileHandleForPath(rootDirectory, path, false);\n return true;\n }\n catch {\n const parts = path.split('/').filter(Boolean);\n let directory = rootDirectory;\n for (const part of parts) {\n try {\n directory = await directory.getDirectoryHandle(part);\n }\n catch {\n return false;\n }\n }\n return true;\n }\n },\n readObject: async (path) => withPathLock(path, async () => {\n const rootDirectory = await navigator.storage.getDirectory();\n const handle = await getFileHandleForPath(rootDirectory, path, false);\n const file = await handle.getFile();\n const text = await file.text();\n return deserialize(text);\n }),\n writeObject: async (path, value) => withPathLock(path, async () => {\n const rootDirectory = await navigator.storage.getDirectory();\n const handle = await getFileHandleForPath(rootDirectory, path, true);\n const text = serialize(value);\n if (typeof text !== 'string') {\n throw new TypeError('serialize() must return a string');\n }\n const writableStream = await handle.createWritable();\n try {\n const encoded = new TextEncoder().encode(text);\n await writableStream.write({ type: 'write', position: 0, data: encoded });\n await writableStream.truncate(encoded.byteLength);\n await writableStream.close();\n }\n catch (error) {\n await writableStream.abort();\n throw error;\n }\n }),\n readIndexObject: async (path) => withPathLock(path, async () => {\n const rootDirectory = await navigator.storage.getDirectory();\n const handle = await getFileHandleForPath(rootDirectory, path, false);\n const file = await handle.getFile();\n const text = await file.text();\n return deserialize(text);\n }),\n writeIndexObject: async (path, value) => withPathLock(path, async () => {\n const rootDirectory = await navigator.storage.getDirectory();\n const handle = await getFileHandleForPath(rootDirectory, path, true);\n const text = serialize(value);\n if (typeof text !== 'string') {\n throw new TypeError('serialize() must return a string');\n }\n const writableStream = await handle.createWritable();\n try {\n const encoded = new TextEncoder().encode(text);\n await writableStream.write({ type: 'write', position: 0, data: encoded });\n await writableStream.truncate(encoded.byteLength);\n await writableStream.close();\n }\n catch (error) {\n await writableStream.abort();\n throw error;\n }\n }),\n listFilesRecursive: async (directoryPath) => {\n const rootDirectory = await navigator.storage.getDirectory();\n const directoryHandle = await ensureDirectoryExists(rootDirectory, directoryPath, false);\n const files = [];\n // @ts-expect-error -- for-await-of on FileSystemDirectoryHandle is not in types yet\n for await (const entry of directoryHandle.values()) {\n if (entry.kind === 'file') {\n files.push(entry.name);\n }\n else if (entry.kind === 'directory') {\n const subFiles = await driver.listFilesRecursive(`${directoryPath}/${entry.name}`);\n files.push(...subFiles.map(f => `${entry.name}/${f}`));\n }\n }\n return files;\n },\n removeEntry: async (path, removeOptions) => {\n const rootDirectory = await navigator.storage.getDirectory();\n const pathParts = path.split('/').filter(Boolean);\n const name = pathParts.pop();\n if (!name)\n throw new Error('Invalid path');\n const parent = pathParts.length > 0\n ? await ensureDirectoryExists(rootDirectory, pathParts.join('/'), false)\n : rootDirectory;\n await parent.removeEntry(name, { recursive: Boolean(removeOptions?.recursive) });\n },\n };\n return createGenericFSAdapter(driver, folderName);\n}\n"],"names":["toSafeFilename","input","name","escapeRegex","withPathLock","path","fn","lockName","createOPFSAdapter","folderName","options","serialize","deserialize","ensureDirectoryExists","rootDirectory","directoryPath","createIfMissing","parts","current","part","getFileHandleForPath","fullPath","fileName","driver","serializeValue","id","key","directory","text","value","handle","writableStream","encoded","error","directoryHandle","files","entry","subFiles","f","removeOptions","pathParts","createGenericFSAdapter"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/index.ts"],"sourcesContent":["import type { Driver } from '@signaldb/generic-fs'\nimport createGenericFSAdapter from '@signaldb/generic-fs'\nimport { serializeValue } from '@signaldb/core'\n\n/**\n * Convert an arbitrary filename into a OPFS safe filename.\n * @param input - The input filename to sanitize.\n * @returns A safe filename.\n */\nfunction toSafeFilename(input: string): string {\n const replacement = '_'\n\n let name = input.normalize('NFC')\n\n const escapeRegex = (s: string) => s.replaceAll(/[-/\\\\^$*+?.()|[\\]{}]/g, String.raw`\\$&`)\n name = name.replaceAll('/', replacement)\n name = name.replaceAll(new RegExp(`${escapeRegex(replacement)}{2,}`, 'g'), replacement)\n\n if (!name) name = 'unnamed'\n\n return name\n}\n\n/**\n * Acquire a lock for a specific file path to ensure exclusive access during read/write operations.\n * @param path - The file path to lock.\n * @param fn - The asynchronous function to execute while holding the lock.\n * @returns The result of the function `fn`.\n */\nasync function withPathLock<T>(path: string, fn: () => Promise<T>): Promise<T> {\n const lockName = `opfs:${path}`\n // Use exclusive mode so reads cannot interleave with writes\n return navigator.locks.request(lockName, { mode: 'exclusive' }, fn)\n}\n\n/**\n * Creates a persistence adapter for managing a SignalDB collection using the\n * Origin Private File System (OPFS). This adapter allows data to be stored and managed\n * directly in the browser's file system with support for customizable serialization\n * and deserialization.\n * @template T - The type of the items in the collection.\n * @template I - The type of the unique identifier for the items.\n * @param folderName - The name of the file in OPFS where data will be stored.\n * @param options - Optional configuration for serialization and deserialization.\n * @param options.serialize - A function to serialize items to a string (default: `JSON.stringify`).\n * @param options.deserialize - A function to deserialize a string into items (default: `JSON.parse`).\n * @returns A SignalDB persistence adapter for managing data in OPFS.\n * @example\n * import createOPFSAdapter from './createOPFSAdapter';\n * import { Collection } from '@signaldb/core';\n *\n * const adapter = createOPFSAdapter('myCollection.json', {\n * serialize: (items) => JSON.stringify(items, null, 2), // Pretty-print JSON\n * deserialize: (itemsString) => JSON.parse(itemsString), // Default JSON parse\n * });\n *\n * const collection = new Collection({\n * persistence: adapter,\n * });\n *\n * // Perform operations on the collection, and changes will be reflected in the OPFS file.\n */\nexport default function createOPFSAdapter<\n T extends { id: I } & Record<string, any>,\n I,\n>(\n folderName: string,\n options?: {\n serialize?: (data: any) => string,\n deserialize?: (input: string) => any,\n },\n) {\n const {\n serialize = JSON.stringify,\n deserialize = JSON.parse,\n } = options || {}\n\n const ensureDirectoryExists = async (\n rootDirectory: FileSystemDirectoryHandle,\n directoryPath: string,\n createIfMissing: boolean,\n ) => {\n const parts = directoryPath.split('/').filter(Boolean)\n let current = rootDirectory\n for (const part of parts) {\n current = await current.getDirectoryHandle(part, { create: createIfMissing })\n }\n return current\n }\n\n const getFileHandleForPath = async (\n rootDirectory: FileSystemDirectoryHandle,\n fullPath: string,\n createIfMissing: boolean,\n ): Promise<FileSystemFileHandle> => {\n const parts = fullPath.split('/').filter(Boolean)\n const fileName = parts.pop() as string\n const directoryHandle = await ensureDirectoryExists(rootDirectory, parts.join('/'), createIfMissing)\n return directoryHandle.getFileHandle(fileName, { create: createIfMissing })\n }\n\n const driver: Driver<T, I> = {\n fileNameForId: id => Promise.resolve(toSafeFilename(serializeValue(id) as string)),\n fileNameForIndexKey: key => Promise.resolve(toSafeFilename(key)),\n joinPath: (...parts) => Promise.resolve(parts.join('/')),\n ensureDir: async (directoryPath) => {\n const rootDirectory = await navigator.storage.getDirectory()\n await ensureDirectoryExists(rootDirectory, directoryPath, true)\n },\n\n fileExists: async (path) => {\n const rootDirectory = await navigator.storage.getDirectory()\n try {\n await getFileHandleForPath(rootDirectory, path, false)\n return true\n } catch {\n const parts = path.split('/').filter(Boolean)\n let directory = rootDirectory\n for (const part of parts) {\n try {\n directory = await directory.getDirectoryHandle(part)\n } catch {\n return false\n }\n }\n return true\n }\n },\n\n readObject: async path => withPathLock(path, async () => {\n const rootDirectory = await navigator.storage.getDirectory()\n const handle = await getFileHandleForPath(rootDirectory, path, false)\n const file = await handle.getFile()\n const text = await file.text()\n return deserialize(text)\n }),\n\n writeObject: async (path, value) => withPathLock(path, async () => {\n const rootDirectory = await navigator.storage.getDirectory()\n const handle = await getFileHandleForPath(rootDirectory, path, true)\n const text = serialize(value)\n if (typeof text !== 'string') {\n throw new TypeError('serialize() must return a string')\n }\n const writableStream = await handle.createWritable()\n try {\n const encoded = new TextEncoder().encode(text)\n await writableStream.write({ type: 'write', position: 0, data: encoded })\n await writableStream.truncate(encoded.byteLength)\n await writableStream.close()\n } catch (error) {\n await writableStream.abort()\n throw error\n }\n }),\n\n readIndexObject: async path => withPathLock(path, async () => {\n const rootDirectory = await navigator.storage.getDirectory()\n const handle = await getFileHandleForPath(rootDirectory, path, false)\n const file = await handle.getFile()\n const text = await file.text()\n return deserialize(text)\n }),\n\n writeIndexObject: async (path, value) => withPathLock(path, async () => {\n const rootDirectory = await navigator.storage.getDirectory()\n const handle = await getFileHandleForPath(rootDirectory, path, true)\n const text = serialize(value)\n if (typeof text !== 'string') {\n throw new TypeError('serialize() must return a string')\n }\n const writableStream = await handle.createWritable()\n try {\n const encoded = new TextEncoder().encode(text)\n await writableStream.write({ type: 'write', position: 0, data: encoded })\n await writableStream.truncate(encoded.byteLength)\n await writableStream.close()\n } catch (error) {\n await writableStream.abort()\n throw error\n }\n }),\n\n listFilesRecursive: async (directoryPath) => {\n const rootDirectory = await navigator.storage.getDirectory()\n const directoryHandle = await ensureDirectoryExists(rootDirectory, directoryPath, false)\n\n const files: string[] = []\n // @ts-expect-error -- for-await-of on FileSystemDirectoryHandle is not in types yet\n for await (const entry of directoryHandle.values()) {\n if (entry.kind === 'file') {\n files.push(entry.name as string)\n } else if (entry.kind === 'directory') {\n const subFiles = await driver.listFilesRecursive(`${directoryPath}/${entry.name}`)\n files.push(...subFiles.map(f => `${entry.name}/${f}`))\n }\n }\n return files\n },\n\n removeEntry: async (path, removeOptions) => {\n const rootDirectory = await navigator.storage.getDirectory()\n const pathParts = path.split('/').filter(Boolean)\n const name = pathParts.pop()\n if (!name) throw new Error('Invalid path')\n const parent = pathParts.length > 0\n ? await ensureDirectoryExists(rootDirectory, pathParts.join('/'), false)\n : rootDirectory\n await parent.removeEntry(name, { recursive: Boolean(removeOptions?.recursive) })\n },\n }\n return createGenericFSAdapter<T, I>(driver, folderName)\n}\n"],"mappings":"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"}
package/dist/index.umd.js CHANGED
@@ -1,2 +1,2 @@
1
- (function(c,w){typeof exports=="object"&&typeof module<"u"?module.exports=w(require("@signaldb/generic-fs"),require("@signaldb/core")):typeof define=="function"&&define.amd?define(["@signaldb/generic-fs","@signaldb/core"],w):(c=typeof globalThis<"u"?globalThis:c||self,c.SignalDB=w(c.createGenericFSAdapter,c.core))})(this,(function(c,w){"use strict";function p(y){let i=y.normalize("NFC");const g=l=>l.replaceAll(/[-/\\^$*+?.()|[\]{}]/g,String.raw`\$&`);return i=i.replaceAll("/","_"),i=i.replaceAll(new RegExp(`${g("_")}{2,}`,"g"),"_"),i||(i="unnamed"),i}async function f(y,u){const i=`opfs:${y}`;return navigator.locks.request(i,{mode:"exclusive"},u)}function h(y,u){const{serialize:i=JSON.stringify,deserialize:g=JSON.parse}=u||{},l=async(e,a,n)=>{const r=a.split("/").filter(Boolean);let t=e;for(const o of r)t=await t.getDirectoryHandle(o,{create:n});return t},d=async(e,a,n)=>{const r=a.split("/").filter(Boolean),t=r.pop();return(await l(e,r.join("/"),n)).getFileHandle(t,{create:n})},m={fileNameForId:e=>Promise.resolve(p(w.serializeValue(e))),fileNameForIndexKey:e=>Promise.resolve(p(e)),joinPath:(...e)=>Promise.resolve(e.join("/")),ensureDir:async e=>{const a=await navigator.storage.getDirectory();await l(a,e,!0)},fileExists:async e=>{const a=await navigator.storage.getDirectory();try{return await d(a,e,!1),!0}catch{const n=e.split("/").filter(Boolean);let r=a;for(const t of n)try{r=await r.getDirectoryHandle(t)}catch{return!1}return!0}},readObject:async e=>f(e,async()=>{const a=await navigator.storage.getDirectory(),t=await(await(await d(a,e,!1)).getFile()).text();return g(t)}),writeObject:async(e,a)=>f(e,async()=>{const n=await navigator.storage.getDirectory(),r=await d(n,e,!0),t=i(a);if(typeof t!="string")throw new TypeError("serialize() must return a string");const o=await r.createWritable();try{const s=new TextEncoder().encode(t);await o.write({type:"write",position:0,data:s}),await o.truncate(s.byteLength),await o.close()}catch(s){throw await o.abort(),s}}),readIndexObject:async e=>f(e,async()=>{const a=await navigator.storage.getDirectory(),t=await(await(await d(a,e,!1)).getFile()).text();return g(t)}),writeIndexObject:async(e,a)=>f(e,async()=>{const n=await navigator.storage.getDirectory(),r=await d(n,e,!0),t=i(a);if(typeof t!="string")throw new TypeError("serialize() must return a string");const o=await r.createWritable();try{const s=new TextEncoder().encode(t);await o.write({type:"write",position:0,data:s}),await o.truncate(s.byteLength),await o.close()}catch(s){throw await o.abort(),s}}),listFilesRecursive:async e=>{const a=await navigator.storage.getDirectory(),n=await l(a,e,!1),r=[];for await(const t of n.values())if(t.kind==="file")r.push(t.name);else if(t.kind==="directory"){const o=await m.listFilesRecursive(`${e}/${t.name}`);r.push(...o.map(s=>`${t.name}/${s}`))}return r},removeEntry:async(e,a)=>{const n=await navigator.storage.getDirectory(),r=e.split("/").filter(Boolean),t=r.pop();if(!t)throw new Error("Invalid path");await(r.length>0?await l(n,r.join("/"),!1):n).removeEntry(t,{recursive:!!a?.recursive})}};return c(m,y)}return h}));
2
- //# sourceMappingURL=index.umd.js.map
1
+ (function(e,t){typeof exports==`object`&&typeof module<`u`?module.exports=t(require("@signaldb/generic-fs"),require("@signaldb/core")):typeof define==`function`&&define.amd?define([`@signaldb/generic-fs`,`@signaldb/core`],t):(e=typeof globalThis<`u`?globalThis:e||self,e.SignalDB=t(e._signaldb_generic_fs,e._signaldb_core))})(this,function(e,t){var n=Object.create,r=Object.defineProperty,i=Object.getOwnPropertyDescriptor,a=Object.getOwnPropertyNames,o=Object.getPrototypeOf,s=Object.prototype.hasOwnProperty,c=(e,t,n,o)=>{if(t&&typeof t==`object`||typeof t==`function`)for(var c=a(t),l=0,u=c.length,d;l<u;l++)d=c[l],!s.call(e,d)&&d!==n&&r(e,d,{get:(e=>t[e]).bind(null,d),enumerable:!(o=i(t,d))||o.enumerable});return e};e=((e,t,i)=>(i=e==null?{}:n(o(e)),c(t||!e||!e.__esModule?r(i,`default`,{value:e,enumerable:!0}):i,e)))(e);function l(e){let t=e.normalize(`NFC`);return t=t.replaceAll(`/`,`_`),t=t.replaceAll(RegExp(`${(e=>e.replaceAll(/[-/\\^$*+?.()|[\]{}]/g,String.raw`\$&`))(`_`)}{2,}`,`g`),`_`),t||=`unnamed`,t}async function u(e,t){let n=`opfs:${e}`;return navigator.locks.request(n,{mode:`exclusive`},t)}function d(n,r){let{serialize:i=JSON.stringify,deserialize:a=JSON.parse}=r||{},o=async(e,t,n)=>{let r=t.split(`/`).filter(Boolean),i=e;for(let e of r)i=await i.getDirectoryHandle(e,{create:n});return i},s=async(e,t,n)=>{let r=t.split(`/`).filter(Boolean),i=r.pop();return(await o(e,r.join(`/`),n)).getFileHandle(i,{create:n})},c={fileNameForId:e=>Promise.resolve(l((0,t.serializeValue)(e))),fileNameForIndexKey:e=>Promise.resolve(l(e)),joinPath:(...e)=>Promise.resolve(e.join(`/`)),ensureDir:async e=>{await o(await navigator.storage.getDirectory(),e,!0)},fileExists:async e=>{let t=await navigator.storage.getDirectory();try{return await s(t,e,!1),!0}catch{let n=e.split(`/`).filter(Boolean),r=t;for(let e of n)try{r=await r.getDirectoryHandle(e)}catch{return!1}return!0}},readObject:async e=>u(e,async()=>a(await(await(await s(await navigator.storage.getDirectory(),e,!1)).getFile()).text())),writeObject:async(e,t)=>u(e,async()=>{let n=await s(await navigator.storage.getDirectory(),e,!0),r=i(t);if(typeof r!=`string`)throw TypeError(`serialize() must return a string`);let a=await n.createWritable();try{let e=new TextEncoder().encode(r);await a.write({type:`write`,position:0,data:e}),await a.truncate(e.byteLength),await a.close()}catch(e){throw await a.abort(),e}}),readIndexObject:async e=>u(e,async()=>a(await(await(await s(await navigator.storage.getDirectory(),e,!1)).getFile()).text())),writeIndexObject:async(e,t)=>u(e,async()=>{let n=await s(await navigator.storage.getDirectory(),e,!0),r=i(t);if(typeof r!=`string`)throw TypeError(`serialize() must return a string`);let a=await n.createWritable();try{let e=new TextEncoder().encode(r);await a.write({type:`write`,position:0,data:e}),await a.truncate(e.byteLength),await a.close()}catch(e){throw await a.abort(),e}}),listFilesRecursive:async e=>{let t=await o(await navigator.storage.getDirectory(),e,!1),n=[];for await(let r of t.values())if(r.kind===`file`)n.push(r.name);else if(r.kind===`directory`){let t=await c.listFilesRecursive(`${e}/${r.name}`);n.push(...t.map(e=>`${r.name}/${e}`))}return n},removeEntry:async(e,t)=>{let n=await navigator.storage.getDirectory(),r=e.split(`/`).filter(Boolean),i=r.pop();if(!i)throw Error(`Invalid path`);await(r.length>0?await o(n,r.join(`/`),!1):n).removeEntry(i,{recursive:!!t?.recursive})}};return(0,e.default)(c,n)}return d});
2
+ //# sourceMappingURL=index.umd.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.umd.js","sources":["../src/index.ts"],"sourcesContent":["import createGenericFSAdapter from '@signaldb/generic-fs';\nimport { serializeValue } from '@signaldb/core';\n/**\n * Convert an arbitrary filename into a OPFS safe filename.\n * @param input - The input filename to sanitize.\n * @returns A safe filename.\n */\nfunction toSafeFilename(input) {\n const replacement = '_';\n let name = input.normalize('NFC');\n const escapeRegex = (s) => s.replaceAll(/[-/\\\\^$*+?.()|[\\]{}]/g, String.raw `\\$&`);\n name = name.replaceAll('/', replacement);\n name = name.replaceAll(new RegExp(`${escapeRegex(replacement)}{2,}`, 'g'), replacement);\n if (!name)\n name = 'unnamed';\n return name;\n}\n/**\n * Acquire a lock for a specific file path to ensure exclusive access during read/write operations.\n * @param path - The file path to lock.\n * @param fn - The asynchronous function to execute while holding the lock.\n * @returns The result of the function `fn`.\n */\nasync function withPathLock(path, fn) {\n const lockName = `opfs:${path}`;\n // Use exclusive mode so reads cannot interleave with writes\n return navigator.locks.request(lockName, { mode: 'exclusive' }, fn);\n}\n/**\n * Creates a persistence adapter for managing a SignalDB collection using the\n * Origin Private File System (OPFS). This adapter allows data to be stored and managed\n * directly in the browser's file system with support for customizable serialization\n * and deserialization.\n * @template T - The type of the items in the collection.\n * @template I - The type of the unique identifier for the items.\n * @param folderName - The name of the file in OPFS where data will be stored.\n * @param options - Optional configuration for serialization and deserialization.\n * @param options.serialize - A function to serialize items to a string (default: `JSON.stringify`).\n * @param options.deserialize - A function to deserialize a string into items (default: `JSON.parse`).\n * @returns A SignalDB persistence adapter for managing data in OPFS.\n * @example\n * import createOPFSAdapter from './createOPFSAdapter';\n * import { Collection } from '@signaldb/core';\n *\n * const adapter = createOPFSAdapter('myCollection.json', {\n * serialize: (items) => JSON.stringify(items, null, 2), // Pretty-print JSON\n * deserialize: (itemsString) => JSON.parse(itemsString), // Default JSON parse\n * });\n *\n * const collection = new Collection({\n * persistence: adapter,\n * });\n *\n * // Perform operations on the collection, and changes will be reflected in the OPFS file.\n */\nexport default function createOPFSAdapter(folderName, options) {\n const { serialize = JSON.stringify, deserialize = JSON.parse, } = options || {};\n const ensureDirectoryExists = async (rootDirectory, directoryPath, createIfMissing) => {\n const parts = directoryPath.split('/').filter(Boolean);\n let current = rootDirectory;\n for (const part of parts) {\n current = await current.getDirectoryHandle(part, { create: createIfMissing });\n }\n return current;\n };\n const getFileHandleForPath = async (rootDirectory, fullPath, createIfMissing) => {\n const parts = fullPath.split('/').filter(Boolean);\n const fileName = parts.pop();\n const directoryHandle = await ensureDirectoryExists(rootDirectory, parts.join('/'), createIfMissing);\n return directoryHandle.getFileHandle(fileName, { create: createIfMissing });\n };\n const driver = {\n fileNameForId: id => Promise.resolve(toSafeFilename(serializeValue(id))),\n fileNameForIndexKey: key => Promise.resolve(toSafeFilename(key)),\n joinPath: (...parts) => Promise.resolve(parts.join('/')),\n ensureDir: async (directoryPath) => {\n const rootDirectory = await navigator.storage.getDirectory();\n await ensureDirectoryExists(rootDirectory, directoryPath, true);\n },\n fileExists: async (path) => {\n const rootDirectory = await navigator.storage.getDirectory();\n try {\n await getFileHandleForPath(rootDirectory, path, false);\n return true;\n }\n catch {\n const parts = path.split('/').filter(Boolean);\n let directory = rootDirectory;\n for (const part of parts) {\n try {\n directory = await directory.getDirectoryHandle(part);\n }\n catch {\n return false;\n }\n }\n return true;\n }\n },\n readObject: async (path) => withPathLock(path, async () => {\n const rootDirectory = await navigator.storage.getDirectory();\n const handle = await getFileHandleForPath(rootDirectory, path, false);\n const file = await handle.getFile();\n const text = await file.text();\n return deserialize(text);\n }),\n writeObject: async (path, value) => withPathLock(path, async () => {\n const rootDirectory = await navigator.storage.getDirectory();\n const handle = await getFileHandleForPath(rootDirectory, path, true);\n const text = serialize(value);\n if (typeof text !== 'string') {\n throw new TypeError('serialize() must return a string');\n }\n const writableStream = await handle.createWritable();\n try {\n const encoded = new TextEncoder().encode(text);\n await writableStream.write({ type: 'write', position: 0, data: encoded });\n await writableStream.truncate(encoded.byteLength);\n await writableStream.close();\n }\n catch (error) {\n await writableStream.abort();\n throw error;\n }\n }),\n readIndexObject: async (path) => withPathLock(path, async () => {\n const rootDirectory = await navigator.storage.getDirectory();\n const handle = await getFileHandleForPath(rootDirectory, path, false);\n const file = await handle.getFile();\n const text = await file.text();\n return deserialize(text);\n }),\n writeIndexObject: async (path, value) => withPathLock(path, async () => {\n const rootDirectory = await navigator.storage.getDirectory();\n const handle = await getFileHandleForPath(rootDirectory, path, true);\n const text = serialize(value);\n if (typeof text !== 'string') {\n throw new TypeError('serialize() must return a string');\n }\n const writableStream = await handle.createWritable();\n try {\n const encoded = new TextEncoder().encode(text);\n await writableStream.write({ type: 'write', position: 0, data: encoded });\n await writableStream.truncate(encoded.byteLength);\n await writableStream.close();\n }\n catch (error) {\n await writableStream.abort();\n throw error;\n }\n }),\n listFilesRecursive: async (directoryPath) => {\n const rootDirectory = await navigator.storage.getDirectory();\n const directoryHandle = await ensureDirectoryExists(rootDirectory, directoryPath, false);\n const files = [];\n // @ts-expect-error -- for-await-of on FileSystemDirectoryHandle is not in types yet\n for await (const entry of directoryHandle.values()) {\n if (entry.kind === 'file') {\n files.push(entry.name);\n }\n else if (entry.kind === 'directory') {\n const subFiles = await driver.listFilesRecursive(`${directoryPath}/${entry.name}`);\n files.push(...subFiles.map(f => `${entry.name}/${f}`));\n }\n }\n return files;\n },\n removeEntry: async (path, removeOptions) => {\n const rootDirectory = await navigator.storage.getDirectory();\n const pathParts = path.split('/').filter(Boolean);\n const name = pathParts.pop();\n if (!name)\n throw new Error('Invalid path');\n const parent = pathParts.length > 0\n ? await ensureDirectoryExists(rootDirectory, pathParts.join('/'), false)\n : rootDirectory;\n await parent.removeEntry(name, { recursive: Boolean(removeOptions?.recursive) });\n },\n };\n return createGenericFSAdapter(driver, folderName);\n}\n"],"names":["toSafeFilename","input","name","escapeRegex","s","withPathLock","path","fn","lockName","createOPFSAdapter","folderName","options","serialize","deserialize","ensureDirectoryExists","rootDirectory","directoryPath","createIfMissing","parts","current","part","getFileHandleForPath","fullPath","fileName","driver","serializeValue","id","key","directory","text","value","handle","writableStream","encoded","error","directoryHandle","files","entry","subFiles","f","removeOptions","pathParts","createGenericFSAdapter"],"mappings":"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"}
1
+ {"version":3,"file":"index.umd.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["import type { Driver } from '@signaldb/generic-fs'\nimport createGenericFSAdapter from '@signaldb/generic-fs'\nimport { serializeValue } from '@signaldb/core'\n\n/**\n * Convert an arbitrary filename into a OPFS safe filename.\n * @param input - The input filename to sanitize.\n * @returns A safe filename.\n */\nfunction toSafeFilename(input: string): string {\n const replacement = '_'\n\n let name = input.normalize('NFC')\n\n const escapeRegex = (s: string) => s.replaceAll(/[-/\\\\^$*+?.()|[\\]{}]/g, String.raw`\\$&`)\n name = name.replaceAll('/', replacement)\n name = name.replaceAll(new RegExp(`${escapeRegex(replacement)}{2,}`, 'g'), replacement)\n\n if (!name) name = 'unnamed'\n\n return name\n}\n\n/**\n * Acquire a lock for a specific file path to ensure exclusive access during read/write operations.\n * @param path - The file path to lock.\n * @param fn - The asynchronous function to execute while holding the lock.\n * @returns The result of the function `fn`.\n */\nasync function withPathLock<T>(path: string, fn: () => Promise<T>): Promise<T> {\n const lockName = `opfs:${path}`\n // Use exclusive mode so reads cannot interleave with writes\n return navigator.locks.request(lockName, { mode: 'exclusive' }, fn)\n}\n\n/**\n * Creates a persistence adapter for managing a SignalDB collection using the\n * Origin Private File System (OPFS). This adapter allows data to be stored and managed\n * directly in the browser's file system with support for customizable serialization\n * and deserialization.\n * @template T - The type of the items in the collection.\n * @template I - The type of the unique identifier for the items.\n * @param folderName - The name of the file in OPFS where data will be stored.\n * @param options - Optional configuration for serialization and deserialization.\n * @param options.serialize - A function to serialize items to a string (default: `JSON.stringify`).\n * @param options.deserialize - A function to deserialize a string into items (default: `JSON.parse`).\n * @returns A SignalDB persistence adapter for managing data in OPFS.\n * @example\n * import createOPFSAdapter from './createOPFSAdapter';\n * import { Collection } from '@signaldb/core';\n *\n * const adapter = createOPFSAdapter('myCollection.json', {\n * serialize: (items) => JSON.stringify(items, null, 2), // Pretty-print JSON\n * deserialize: (itemsString) => JSON.parse(itemsString), // Default JSON parse\n * });\n *\n * const collection = new Collection({\n * persistence: adapter,\n * });\n *\n * // Perform operations on the collection, and changes will be reflected in the OPFS file.\n */\nexport default function createOPFSAdapter<\n T extends { id: I } & Record<string, any>,\n I,\n>(\n folderName: string,\n options?: {\n serialize?: (data: any) => string,\n deserialize?: (input: string) => any,\n },\n) {\n const {\n serialize = JSON.stringify,\n deserialize = JSON.parse,\n } = options || {}\n\n const ensureDirectoryExists = async (\n rootDirectory: FileSystemDirectoryHandle,\n directoryPath: string,\n createIfMissing: boolean,\n ) => {\n const parts = directoryPath.split('/').filter(Boolean)\n let current = rootDirectory\n for (const part of parts) {\n current = await current.getDirectoryHandle(part, { create: createIfMissing })\n }\n return current\n }\n\n const getFileHandleForPath = async (\n rootDirectory: FileSystemDirectoryHandle,\n fullPath: string,\n createIfMissing: boolean,\n ): Promise<FileSystemFileHandle> => {\n const parts = fullPath.split('/').filter(Boolean)\n const fileName = parts.pop() as string\n const directoryHandle = await ensureDirectoryExists(rootDirectory, parts.join('/'), createIfMissing)\n return directoryHandle.getFileHandle(fileName, { create: createIfMissing })\n }\n\n const driver: Driver<T, I> = {\n fileNameForId: id => Promise.resolve(toSafeFilename(serializeValue(id) as string)),\n fileNameForIndexKey: key => Promise.resolve(toSafeFilename(key)),\n joinPath: (...parts) => Promise.resolve(parts.join('/')),\n ensureDir: async (directoryPath) => {\n const rootDirectory = await navigator.storage.getDirectory()\n await ensureDirectoryExists(rootDirectory, directoryPath, true)\n },\n\n fileExists: async (path) => {\n const rootDirectory = await navigator.storage.getDirectory()\n try {\n await getFileHandleForPath(rootDirectory, path, false)\n return true\n } catch {\n const parts = path.split('/').filter(Boolean)\n let directory = rootDirectory\n for (const part of parts) {\n try {\n directory = await directory.getDirectoryHandle(part)\n } catch {\n return false\n }\n }\n return true\n }\n },\n\n readObject: async path => withPathLock(path, async () => {\n const rootDirectory = await navigator.storage.getDirectory()\n const handle = await getFileHandleForPath(rootDirectory, path, false)\n const file = await handle.getFile()\n const text = await file.text()\n return deserialize(text)\n }),\n\n writeObject: async (path, value) => withPathLock(path, async () => {\n const rootDirectory = await navigator.storage.getDirectory()\n const handle = await getFileHandleForPath(rootDirectory, path, true)\n const text = serialize(value)\n if (typeof text !== 'string') {\n throw new TypeError('serialize() must return a string')\n }\n const writableStream = await handle.createWritable()\n try {\n const encoded = new TextEncoder().encode(text)\n await writableStream.write({ type: 'write', position: 0, data: encoded })\n await writableStream.truncate(encoded.byteLength)\n await writableStream.close()\n } catch (error) {\n await writableStream.abort()\n throw error\n }\n }),\n\n readIndexObject: async path => withPathLock(path, async () => {\n const rootDirectory = await navigator.storage.getDirectory()\n const handle = await getFileHandleForPath(rootDirectory, path, false)\n const file = await handle.getFile()\n const text = await file.text()\n return deserialize(text)\n }),\n\n writeIndexObject: async (path, value) => withPathLock(path, async () => {\n const rootDirectory = await navigator.storage.getDirectory()\n const handle = await getFileHandleForPath(rootDirectory, path, true)\n const text = serialize(value)\n if (typeof text !== 'string') {\n throw new TypeError('serialize() must return a string')\n }\n const writableStream = await handle.createWritable()\n try {\n const encoded = new TextEncoder().encode(text)\n await writableStream.write({ type: 'write', position: 0, data: encoded })\n await writableStream.truncate(encoded.byteLength)\n await writableStream.close()\n } catch (error) {\n await writableStream.abort()\n throw error\n }\n }),\n\n listFilesRecursive: async (directoryPath) => {\n const rootDirectory = await navigator.storage.getDirectory()\n const directoryHandle = await ensureDirectoryExists(rootDirectory, directoryPath, false)\n\n const files: string[] = []\n // @ts-expect-error -- for-await-of on FileSystemDirectoryHandle is not in types yet\n for await (const entry of directoryHandle.values()) {\n if (entry.kind === 'file') {\n files.push(entry.name as string)\n } else if (entry.kind === 'directory') {\n const subFiles = await driver.listFilesRecursive(`${directoryPath}/${entry.name}`)\n files.push(...subFiles.map(f => `${entry.name}/${f}`))\n }\n }\n return files\n },\n\n removeEntry: async (path, removeOptions) => {\n const rootDirectory = await navigator.storage.getDirectory()\n const pathParts = path.split('/').filter(Boolean)\n const name = pathParts.pop()\n if (!name) throw new Error('Invalid path')\n const parent = pathParts.length > 0\n ? await ensureDirectoryExists(rootDirectory, pathParts.join('/'), false)\n : rootDirectory\n await parent.removeEntry(name, { recursive: Boolean(removeOptions?.recursive) })\n },\n }\n return createGenericFSAdapter<T, I>(driver, folderName)\n}\n"],"mappings":"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"}
@@ -0,0 +1,55 @@
1
+ export type IndexDelta<I> = {
2
+ adds: Map<string, Set<I>>;
3
+ removes: Map<string, Set<I>>;
4
+ };
5
+ /**
6
+ * Records a single index mutation into the delta map for a given indexed field.
7
+ *
8
+ * The delta map is grouped by `fieldPath`, and then by stringified key (`rawKey`).
9
+ * Each key maps to a set of identifiers to add/remove for that key.
10
+ * @param deltas - Accumulator of per-field index deltas.
11
+ * @param fieldPath - Dot-path of the indexed field (used as the delta bucket key).
12
+ * @param changeKind - Whether this identifier should be added to or removed from the index key.
13
+ * @param rawKey - The stringified index key value.
14
+ * @param identifier - The item identifier to add/remove for `rawKey`.
15
+ */
16
+ export declare function addToDelta<I>(deltas: Map<string, IndexDelta<I>>, fieldPath: string, changeKind: 'add' | 'remove', rawKey: string, identifier: I): void;
17
+ /**
18
+ * Computes the delta for a change in a single indexed field.
19
+ *
20
+ * Values are coerced to string keys (with `null`/`undefined` treated as "no key").
21
+ * If the derived keys are equal, no delta is recorded.
22
+ * @param deltas - Accumulator of per-field index deltas.
23
+ * @param fieldPath - Dot-path of the indexed field being tracked.
24
+ * @param oldValue - Previous value of the field.
25
+ * @param newValue - Next value of the field.
26
+ * @param identifier - The item identifier affected by the change.
27
+ */
28
+ export declare function addDeltaForChange<I>(deltas: Map<string, IndexDelta<I>>, fieldPath: string, oldValue: any, newValue: any, identifier: I): void;
29
+ /**
30
+ * Accumulates index deltas for an upsert operation.
31
+ *
32
+ * - If `existingItem` is present, compares each maintained index field between the old and new item,
33
+ * recording removes/adds for key changes.
34
+ * - If `existingItem` is absent, treats the operation as an insert and records adds for each
35
+ * maintained index field that has a non-null value.
36
+ * @param deltas - Accumulator of per-field index deltas.
37
+ * @param indicesToMaintain - List of dot-paths for fields that are indexed.
38
+ * @param existingItem - Previous stored item (if any).
39
+ * @param nextItem - Incoming item to upsert.
40
+ */
41
+ export declare const accumulateUpsertDelta: <T extends {
42
+ id: I;
43
+ }, I>(deltas: Map<string, IndexDelta<I>>, indicesToMaintain: string[], existingItem: T | undefined, nextItem: T) => void;
44
+ /**
45
+ * Accumulates index deltas for a remove/delete operation.
46
+ *
47
+ * For each maintained index field, records a removal of the item's identifier from the
48
+ * corresponding stringified key (skipping `null`/`undefined` values).
49
+ * @param deltas - Accumulator of per-field index deltas.
50
+ * @param indicesToMaintain - List of dot-paths for fields that are indexed.
51
+ * @param existingItem - The item being removed.
52
+ */
53
+ export declare function accumulateRemoveDelta<T extends {
54
+ id: I;
55
+ }, I>(deltas: Map<string, IndexDelta<I>>, indicesToMaintain: string[], existingItem: T): void;