@delphington/persistent-buckets 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Delph
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,48 @@
1
+ # persistent-buckets
2
+
3
+ Versioned `localStorage` buckets backed by shared Vue refs.
4
+
5
+ ## Usage
6
+
7
+ ```ts
8
+ import { defineBucket } from '@delphington/persistent-buckets';
9
+
10
+ type Preferences = {
11
+ theme: 'light' | 'dark';
12
+ };
13
+
14
+ export const preferences = defineBucket<Preferences>({
15
+ key: 'example:preferences',
16
+ version: 0,
17
+ initial: { theme: 'light' },
18
+ migrate(version, stored) {
19
+ if (version !== 0) throw new Error('Unsupported preferences version');
20
+ return stored as Preferences;
21
+ },
22
+ });
23
+ ```
24
+
25
+ `state` is a shared Vue ref. Changes must be saved explicitly:
26
+
27
+ ```ts
28
+ const state = preferences.state;
29
+ state.value.theme = 'dark';
30
+ preferences.save();
31
+ ```
32
+
33
+ Bucket modules self-register when they are imported. Import every bucket at the application's entry point so aggregate operations always use the complete set:
34
+
35
+ ```ts
36
+ import './persistence/buckets/history';
37
+ import './persistence/buckets/preferences';
38
+ ```
39
+
40
+ Vite applications can instead eagerly import a bucket directory:
41
+
42
+ ```ts
43
+ import.meta.glob('./persistence/buckets/*.ts', { eager: true });
44
+ ```
45
+
46
+ Only imported buckets are included by `persistence.dump()`, `persistence.restore()`, `persistence.reset()`, and `persistence.prune()`.
47
+
48
+ Bucket data must be JSON-serializable. If `localStorage` is unavailable, data is kept in memory and `persistence.ephemeral` is `true`.
@@ -0,0 +1,69 @@
1
+ import { type Ref } from 'vue';
2
+ /** Defines a bucket */
3
+ export type BucketDefinition<T> = {
4
+ /** A unique localStorage key. */
5
+ readonly key: string;
6
+ /** The schema version written by `save`. */
7
+ readonly version: number;
8
+ /** The value returned when no valid stored value exists. */
9
+ readonly initial: T;
10
+ /**
11
+ * Validates stored data and migrates older versions to the current shape.
12
+ * @param version The schema version of the stored data
13
+ * @param stored The untrusted value read from storage
14
+ * @returns Data in the current bucket shape
15
+ */
16
+ migrate: (version: number, stored: unknown) => T;
17
+ /**
18
+ * Removes obsolete data in place.
19
+ * @returns The number of removed records
20
+ */
21
+ prune?: (data: T) => number;
22
+ };
23
+ /** A user-operatable bucket */
24
+ export type Bucket<T> = {
25
+ /** The bucket's key */
26
+ readonly key: string;
27
+ readonly state: Ref<T>;
28
+ dump: () => PersistedBucket<T>;
29
+ prune: () => number;
30
+ reset: () => T;
31
+ restore: (stored: unknown) => T;
32
+ save: () => void;
33
+ };
34
+ /**
35
+ * Record of what a bucket is for syncronise operations across all buckets
36
+ */
37
+ export type RegisteredBucket = {
38
+ /** The bucket's storage key */
39
+ readonly key: string;
40
+ /** The function to dump/export the bucket */
41
+ dump: () => PersistedBucket<unknown>;
42
+ /** The function which parses an unknown JSON object, migrates if necessary */
43
+ parse: (stored: unknown) => unknown;
44
+ /** The user-provided callback function of how to prune records (for operations like soft-deletion handling) */
45
+ prune: () => number;
46
+ /** Clears the bucket */
47
+ reset: () => unknown;
48
+ /** Restores the bucket's state to the provided value */
49
+ restore: (stored: unknown) => unknown;
50
+ /** Same as restore */
51
+ restoreData: (data: unknown) => unknown;
52
+ };
53
+ /**
54
+ * The versioned envelope used to store a bucket.
55
+ */
56
+ export type PersistedBucket<T> = {
57
+ version: number;
58
+ data: T;
59
+ };
60
+ export declare const buckets: Map<string, RegisteredBucket>;
61
+ /**
62
+ * Creates a persistent bucket and registers it for aggregate operations.
63
+ * `state` returns one shared Vue ref. Mutating that ref does not write to
64
+ * storage until `save()` is called.
65
+ * @param bucket The bucket definition
66
+ * @returns Operations for the defined bucket
67
+ */
68
+ export declare function defineBucket<T>(bucket: BucketDefinition<T>): Bucket<T>;
69
+ //# sourceMappingURL=bucket.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bucket.d.ts","sourceRoot":"","sources":["../src/bucket.ts"],"names":[],"mappings":"AAAA,OAAO,EAAO,KAAK,GAAG,EAAE,MAAM,KAAK,CAAC;AAIpC,uBAAuB;AACvB,MAAM,MAAM,gBAAgB,CAAC,CAAC,IAAI;IAChC,iCAAiC;IACjC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IAErB,4CAA4C;IAC5C,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IAEzB,4DAA4D;IAC5D,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAC;IAEpB;;;;;OAKG;IACH,OAAO,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,KAAK,CAAC,CAAC;IAEjD;;;OAGG;IACH,KAAK,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,MAAM,CAAC;CAC7B,CAAC;AAEF,+BAA+B;AAC/B,MAAM,MAAM,MAAM,CAAC,CAAC,IAAI;IACtB,uBAAuB;IACvB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC;IACvB,IAAI,EAAE,MAAM,eAAe,CAAC,CAAC,CAAC,CAAC;IAC/B,KAAK,EAAE,MAAM,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC,CAAC;IACf,OAAO,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,CAAC,CAAC;IAChC,IAAI,EAAE,MAAM,IAAI,CAAC;CAClB,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,gBAAgB,GAAG;IAC7B,+BAA+B;IAC/B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,6CAA6C;IAC7C,IAAI,EAAE,MAAM,eAAe,CAAC,OAAO,CAAC,CAAC;IACrC,8EAA8E;IAC9E,KAAK,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,OAAO,CAAC;IACpC,+GAA+G;IAC/G,KAAK,EAAE,MAAM,MAAM,CAAC;IACpB,wBAAwB;IACxB,KAAK,EAAE,MAAM,OAAO,CAAC;IACrB,wDAAwD;IACxD,OAAO,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,OAAO,CAAC;IACtC,sBAAsB;IACtB,WAAW,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,OAAO,CAAC;CACzC,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,eAAe,CAAC,CAAC,IAAI;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,CAAC,CAAC;CACT,CAAC;AAEF,eAAO,MAAM,OAAO,+BAAsC,CAAC;AAY3D;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,CAAC,EAAE,MAAM,EAAE,gBAAgB,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,CAoHtE"}
package/dist/bucket.js ADDED
@@ -0,0 +1,119 @@
1
+ import { ref } from 'vue';
2
+ import { storage } from './storage';
3
+ import { bytesToBase64Url, base64UrlToBytes, gzip, gunzip } from './text';
4
+ export const buckets = new Map();
5
+ function isPersistedBucket(value) {
6
+ return (typeof value === 'object' &&
7
+ value !== null &&
8
+ 'version' in value &&
9
+ 'data' in value &&
10
+ typeof value.version === 'number');
11
+ }
12
+ /**
13
+ * Creates a persistent bucket and registers it for aggregate operations.
14
+ * `state` returns one shared Vue ref. Mutating that ref does not write to
15
+ * storage until `save()` is called.
16
+ * @param bucket The bucket definition
17
+ * @returns Operations for the defined bucket
18
+ */
19
+ export function defineBucket(bucket) {
20
+ const state = ref(_load());
21
+ /**
22
+ * returns a copy of the initial data
23
+ */
24
+ function initial() {
25
+ return deepcopy(bucket.initial);
26
+ }
27
+ /**
28
+ * Loads the bucket data, if the data can't be loaded for any reason, loads the initial data
29
+ * @returns the loaded bucket data
30
+ */
31
+ function _load() {
32
+ try {
33
+ const raw = storage.getItem(bucket.key);
34
+ if (raw === null)
35
+ return initial();
36
+ const parsed = JSON.parse(raw);
37
+ if (!isPersistedBucket(parsed))
38
+ return initial();
39
+ return bucket.migrate(parsed.version, parsed.data);
40
+ }
41
+ catch {
42
+ return initial();
43
+ }
44
+ }
45
+ function _save(data) {
46
+ try {
47
+ const value = JSON.stringify({
48
+ version: bucket.version,
49
+ data: data,
50
+ });
51
+ storage.setItem(bucket.key, value);
52
+ }
53
+ catch (error) {
54
+ state.value = _load();
55
+ throw error;
56
+ }
57
+ }
58
+ function save() {
59
+ _save(state.value);
60
+ }
61
+ function prune() {
62
+ if (!bucket.prune)
63
+ return 0;
64
+ const data = deepcopy(state.value);
65
+ const removed = bucket.prune(data);
66
+ if (removed === 0)
67
+ return 0;
68
+ _save(data);
69
+ state.value = data;
70
+ return removed;
71
+ }
72
+ function dump() {
73
+ return {
74
+ version: bucket.version,
75
+ data: _load(),
76
+ };
77
+ }
78
+ function parse(stored) {
79
+ if (!isPersistedBucket(stored))
80
+ throw new Error(`Invalid persisted bucket ${bucket.key}`);
81
+ return bucket.migrate(stored.version, stored.data);
82
+ }
83
+ function restoreData(data) {
84
+ _save(data);
85
+ state.value = data;
86
+ return data;
87
+ }
88
+ function restore(stored) {
89
+ return restoreData(parse(stored));
90
+ }
91
+ function reset() {
92
+ storage.removeItem(bucket.key);
93
+ const value = _load();
94
+ state.value = value;
95
+ return value;
96
+ }
97
+ buckets.set(bucket.key, {
98
+ key: bucket.key,
99
+ dump,
100
+ parse,
101
+ prune,
102
+ reset,
103
+ restore,
104
+ restoreData: (data) => restoreData(data),
105
+ });
106
+ return {
107
+ key: bucket.key,
108
+ state,
109
+ dump,
110
+ prune,
111
+ reset,
112
+ restore,
113
+ save,
114
+ };
115
+ }
116
+ function deepcopy(data) {
117
+ return JSON.parse(JSON.stringify(data));
118
+ }
119
+ //# sourceMappingURL=bucket.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bucket.js","sourceRoot":"","sources":["../src/bucket.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ export { defineBucket, type Bucket } from './bucket';
2
+ export { persistence } from './persistence';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,KAAK,MAAM,EAAE,MAAM,UAAU,CAAC;AACrD,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { defineBucket } from './bucket';
2
+ export { persistence } from './persistence';
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAe,MAAM,UAAU,CAAC;AACrD,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC"}
@@ -0,0 +1,55 @@
1
+ import { type PersistedBucket } from './bucket';
2
+ /**
3
+ * The portable representation of all registered buckets.
4
+ */
5
+ export type PersistedDump = {
6
+ version: 0;
7
+ exportedAt: string;
8
+ buckets: Record<string, PersistedBucket<unknown>>;
9
+ };
10
+ /**
11
+ * Reads and exports every registered bucket from the active storage backend.
12
+ */
13
+ declare function dump(): PersistedDump;
14
+ /**
15
+ * Resets every registered bucket to its initial value.
16
+ */
17
+ declare function reset(): void;
18
+ /**
19
+ * Prunes every registered bucket.
20
+ * @returns The total number of removed records
21
+ */
22
+ declare function prune(): number;
23
+ /**
24
+ * Restores known buckets from a persistence dump. Registered buckets omitted
25
+ * from the dump are reset, and unknown buckets in the dump are ignored. If a
26
+ * write fails, the function attempts to restore the previous data.
27
+ * @param data The untrusted persistence dump to restore
28
+ */
29
+ declare function restore(data: unknown): void;
30
+ /**
31
+ * Exports registered buckets as gzip-compressed text when compression is
32
+ * available, or as formatted JSON otherwise.
33
+ * @param prefix The namespace prepended to compressed exports
34
+ */
35
+ declare function exportText(prefix?: string): Promise<string>;
36
+ /**
37
+ * Restores registered buckets from compressed export text or plain JSON.
38
+ * @param value The exported text to import
39
+ * @param prefix The namespace used by compressed exports
40
+ */
41
+ declare function importText(value: string, prefix?: string): Promise<void>;
42
+ export declare const persistence: {
43
+ /**
44
+ * if true, we've had to fall back to a shim with no persistence
45
+ */
46
+ ephemeral: boolean;
47
+ dump: typeof dump;
48
+ reset: typeof reset;
49
+ restore: typeof restore;
50
+ prune: typeof prune;
51
+ exportText: typeof exportText;
52
+ importText: typeof importText;
53
+ };
54
+ export {};
55
+ //# sourceMappingURL=persistence.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"persistence.d.ts","sourceRoot":"","sources":["../src/persistence.ts"],"names":[],"mappings":"AACA,OAAO,EAAW,KAAK,eAAe,EAAyB,MAAM,UAAU,CAAC;AAOhF;;GAEG;AACH,MAAM,MAAM,aAAa,GAAG;IAC1B,OAAO,EAAE,CAAC,CAAC;IACX,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,OAAO,CAAC,CAAC,CAAC;CACnD,CAAC;AAEF;;GAEG;AACH,iBAAS,IAAI,IAAI,aAAa,CAQ7B;AAED;;GAEG;AACH,iBAAS,KAAK,SAEb;AAED;;;GAGG;AACH,iBAAS,KAAK,WAKb;AAED;;;;;GAKG;AACH,iBAAS,OAAO,CAAC,IAAI,EAAE,OAAO,QAkC7B;AAED;;;;GAIG;AACH,iBAAe,UAAU,CAAC,MAAM,SAAY,mBAQ3C;AAED;;;;GAIG;AACH,iBAAe,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,SAAY,iBAQ1D;AAED,eAAO,MAAM,WAAW;IACtB;;OAEG;;;;;;;;CAUJ,CAAC"}
@@ -0,0 +1,104 @@
1
+ import { storage, MemoryStorage } from './storage';
2
+ import { buckets } from './bucket';
3
+ import { bytesToBase64Url, base64UrlToBytes, gzip, gunzip } from './text';
4
+ function isRecord(value) {
5
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
6
+ }
7
+ /**
8
+ * Reads and exports every registered bucket from the active storage backend.
9
+ */
10
+ function dump() {
11
+ return {
12
+ version: 0,
13
+ exportedAt: new Date().toISOString(),
14
+ buckets: Object.fromEntries([...buckets.values()].map((bucket) => [bucket.key, bucket.dump()])),
15
+ };
16
+ }
17
+ /**
18
+ * Resets every registered bucket to its initial value.
19
+ */
20
+ function reset() {
21
+ for (const bucket of buckets.values())
22
+ bucket.reset();
23
+ }
24
+ /**
25
+ * Prunes every registered bucket.
26
+ * @returns The total number of removed records
27
+ */
28
+ function prune() {
29
+ return [...buckets.values()].reduce((removed, bucket) => removed + bucket.prune(), 0);
30
+ }
31
+ /**
32
+ * Restores known buckets from a persistence dump. Registered buckets omitted
33
+ * from the dump are reset, and unknown buckets in the dump are ignored. If a
34
+ * write fails, the function attempts to restore the previous data.
35
+ * @param data The untrusted persistence dump to restore
36
+ */
37
+ function restore(data) {
38
+ if (!isRecord(data) || data.version !== 0 || !isRecord(data.buckets))
39
+ throw new Error('Invalid persistence dump');
40
+ const restored = Object.entries(data.buckets)
41
+ .map(([key, stored]) => {
42
+ const bucket = buckets.get(key);
43
+ if (!bucket)
44
+ return;
45
+ return [bucket, bucket.parse(stored)];
46
+ })
47
+ .filter((entry) => entry !== undefined);
48
+ const previous = [...buckets.values()].map((bucket) => [bucket, bucket.dump()]);
49
+ try {
50
+ reset();
51
+ for (const [bucket, value] of restored)
52
+ bucket.restoreData(value);
53
+ }
54
+ catch (error) {
55
+ try {
56
+ reset();
57
+ for (const [bucket, value] of previous)
58
+ bucket.restore(value);
59
+ }
60
+ catch {
61
+ throw new Error('Import failed and previous data could not be restored');
62
+ }
63
+ throw error;
64
+ }
65
+ }
66
+ /**
67
+ * Exports registered buckets as gzip-compressed text when compression is
68
+ * available, or as formatted JSON otherwise.
69
+ * @param prefix The namespace prepended to compressed exports
70
+ */
71
+ async function exportText(prefix = 'buckets') {
72
+ const data = dump();
73
+ const json = JSON.stringify(data);
74
+ const compressed = await gzip(json);
75
+ if (!compressed)
76
+ return JSON.stringify(data, null, 2);
77
+ return `${prefix}:gzip:${bytesToBase64Url(compressed)}`;
78
+ }
79
+ /**
80
+ * Restores registered buckets from compressed export text or plain JSON.
81
+ * @param value The exported text to import
82
+ * @param prefix The namespace used by compressed exports
83
+ */
84
+ async function importText(value, prefix = 'buckets') {
85
+ const text = value.trim();
86
+ const header = `${prefix}:gzip:`;
87
+ const json = text.startsWith(header)
88
+ ? await gunzip(base64UrlToBytes(text.slice(header.length)))
89
+ : text;
90
+ restore(JSON.parse(json));
91
+ }
92
+ export const persistence = {
93
+ /**
94
+ * if true, we've had to fall back to a shim with no persistence
95
+ */
96
+ ephemeral: storage instanceof MemoryStorage,
97
+ dump,
98
+ reset,
99
+ restore,
100
+ prune,
101
+ exportText,
102
+ importText,
103
+ };
104
+ //# sourceMappingURL=persistence.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"persistence.js","sourceRoot":"","sources":["../src/persistence.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * A in-memory storage backend for when localStorage is not available
3
+ */
4
+ export declare class MemoryStorage implements Storage {
5
+ private items;
6
+ getItem(key: string): string | null;
7
+ setItem(key: string, value: string): void;
8
+ removeItem(key: string): void;
9
+ clear(): void;
10
+ key(index: number): string | null;
11
+ get length(): number;
12
+ }
13
+ /**
14
+ * The active storage backend. This falls back to in-memory storage when
15
+ * localStorage cannot be accessed.
16
+ */
17
+ export declare const storage: MemoryStorage | Storage;
18
+ //# sourceMappingURL=storage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../src/storage.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,qBAAa,aAAc,YAAW,OAAO;IAC3C,OAAO,CAAC,KAAK,CAA6B;IAE1C,OAAO,CAAC,GAAG,EAAE,MAAM;IAInB,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM;IAIlC,UAAU,CAAC,GAAG,EAAE,MAAM;IAItB,KAAK;IAIL,GAAG,CAAC,KAAK,EAAE,MAAM;IAIjB,IAAI,MAAM,WAET;CACF;AAED;;;GAGG;AACH,eAAO,MAAM,OAAO,yBAMhB,CAAC"}
@@ -0,0 +1,37 @@
1
+ /**
2
+ * A in-memory storage backend for when localStorage is not available
3
+ */
4
+ export class MemoryStorage {
5
+ items = new Map();
6
+ getItem(key) {
7
+ return this.items.get(key) ?? null;
8
+ }
9
+ setItem(key, value) {
10
+ this.items.set(key, value);
11
+ }
12
+ removeItem(key) {
13
+ this.items.delete(key);
14
+ }
15
+ clear() {
16
+ this.items.clear();
17
+ }
18
+ key(index) {
19
+ return [...this.items.keys()][index] ?? null;
20
+ }
21
+ get length() {
22
+ return this.items.size;
23
+ }
24
+ }
25
+ /**
26
+ * The active storage backend. This falls back to in-memory storage when
27
+ * localStorage cannot be accessed.
28
+ */
29
+ export const storage = (() => {
30
+ try {
31
+ return globalThis.localStorage ?? new MemoryStorage();
32
+ }
33
+ catch {
34
+ return new MemoryStorage();
35
+ }
36
+ })();
37
+ //# sourceMappingURL=storage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storage.js","sourceRoot":"","sources":["../src/storage.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,OAAO,aAAa;IAChB,KAAK,GAAG,IAAI,GAAG,EAAkB,CAAC;IAE1C,OAAO,CAAC,GAAW;QACjB,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC;IACrC,CAAC;IAED,OAAO,CAAC,GAAW,EAAE,KAAa;QAChC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;IAC7B,CAAC;IAED,UAAU,CAAC,GAAW;QACpB,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IACzB,CAAC;IAED,KAAK;QACH,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;IACrB,CAAC;IAED,GAAG,CAAC,KAAa;QACf,OAAO,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,IAAI,IAAI,CAAC;IAC/C,CAAC;IAED,IAAI,MAAM;QACR,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC;IACzB,CAAC;CACF;AAED;;;GAGG;AACH,MAAM,CAAC,MAAM,OAAO,GAAG,CAAC,GAAG,EAAE;IAC3B,IAAI,CAAC;QACH,OAAO,UAAU,CAAC,YAAY,IAAI,IAAI,aAAa,EAAE,CAAC;IACxD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,aAAa,EAAE,CAAC;IAC7B,CAAC;AACH,CAAC,CAAC,EAAE,CAAC"}
package/dist/text.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ export declare function bytesToBase64Url(bytes: Uint8Array): string;
2
+ export declare function base64UrlToBytes(value: string): Uint8Array<ArrayBuffer>;
3
+ export declare function gzip(value: string): Promise<Uint8Array<ArrayBuffer> | null>;
4
+ export declare function gunzip(bytes: Uint8Array): Promise<string>;
5
+ //# sourceMappingURL=text.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"text.d.ts","sourceRoot":"","sources":["../src/text.ts"],"names":[],"mappings":"AAAA,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,UAAU,UAWjD;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,2BAW7C;AAED,wBAAsB,IAAI,CAAC,KAAK,EAAE,MAAM,2CAOvC;AAED,wBAAsB,MAAM,CAAC,KAAK,EAAE,UAAU,mBAS7C"}
package/dist/text.js ADDED
@@ -0,0 +1,39 @@
1
+ export function bytesToBase64Url(bytes) {
2
+ let binary = '';
3
+ const chunkSize = 0x8000;
4
+ for (let i = 0; i < bytes.length; i += chunkSize)
5
+ binary += String.fromCharCode(...bytes.subarray(i, i + chunkSize));
6
+ return btoa(binary)
7
+ .replaceAll('+', '-')
8
+ .replaceAll('/', '_')
9
+ .replaceAll('=', '');
10
+ }
11
+ export function base64UrlToBytes(value) {
12
+ const base64 = value
13
+ .replaceAll('-', '+')
14
+ .replaceAll('_', '/')
15
+ .padEnd(Math.ceil(value.length / 4) * 4, '=');
16
+ const binary = atob(base64);
17
+ const bytes = new Uint8Array(binary.length);
18
+ for (let i = 0; i < binary.length; i++)
19
+ bytes[i] = binary.charCodeAt(i);
20
+ return bytes;
21
+ }
22
+ export async function gzip(value) {
23
+ if (typeof CompressionStream === 'undefined')
24
+ return null;
25
+ const stream = new Blob([value])
26
+ .stream()
27
+ .pipeThrough(new CompressionStream('gzip'));
28
+ return new Uint8Array(await new Response(stream).arrayBuffer());
29
+ }
30
+ export async function gunzip(bytes) {
31
+ if (typeof DecompressionStream === 'undefined')
32
+ throw new Error('Compressed imports are not supported by this browser');
33
+ const buffer = new Uint8Array(bytes).buffer;
34
+ const stream = new Blob([buffer])
35
+ .stream()
36
+ .pipeThrough(new DecompressionStream('gzip'));
37
+ return await new Response(stream).text();
38
+ }
39
+ //# sourceMappingURL=text.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"text.js","sourceRoot":"","sources":["../src/text.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,gBAAgB,CAAC,KAAiB;IAChD,IAAI,MAAM,GAAG,EAAE,CAAC;IAChB,MAAM,SAAS,GAAG,MAAM,CAAC;IAEzB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,IAAI,SAAS;QAC9C,MAAM,IAAI,MAAM,CAAC,YAAY,CAAC,GAAG,KAAK,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC,GAAG,SAAS,CAAC,CAAC,CAAC;IAErE,OAAO,IAAI,CAAC,MAAM,CAAC;SAChB,UAAU,CAAC,GAAG,EAAE,GAAG,CAAC;SACpB,UAAU,CAAC,GAAG,EAAE,GAAG,CAAC;SACpB,UAAU,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;AACzB,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,KAAa;IAC5C,MAAM,MAAM,GAAG,KAAK;SACjB,UAAU,CAAC,GAAG,EAAE,GAAG,CAAC;SACpB,UAAU,CAAC,GAAG,EAAE,GAAG,CAAC;SACpB,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,GAAG,CAAC,EAAE,GAAG,CAAC,CAAC;IAChD,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC;IAC5B,MAAM,KAAK,GAAG,IAAI,UAAU,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAE5C,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,EAAE;QAAE,KAAK,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;IAExE,OAAO,KAAK,CAAC;AACf,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,IAAI,CAAC,KAAa;IACtC,IAAI,OAAO,iBAAiB,KAAK,WAAW;QAAE,OAAO,IAAI,CAAC;IAE1D,MAAM,MAAM,GAAG,IAAI,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC;SAC7B,MAAM,EAAE;SACR,WAAW,CAAC,IAAI,iBAAiB,CAAC,MAAM,CAAC,CAAC,CAAC;IAC9C,OAAO,IAAI,UAAU,CAAC,MAAM,IAAI,QAAQ,CAAC,MAAM,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC;AAClE,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,MAAM,CAAC,KAAiB;IAC5C,IAAI,OAAO,mBAAmB,KAAK,WAAW;QAC5C,MAAM,IAAI,KAAK,CAAC,sDAAsD,CAAC,CAAC;IAE1E,MAAM,MAAM,GAAG,IAAI,UAAU,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC;IAC5C,MAAM,MAAM,GAAG,IAAI,IAAI,CAAC,CAAC,MAAM,CAAC,CAAC;SAC9B,MAAM,EAAE;SACR,WAAW,CAAC,IAAI,mBAAmB,CAAC,MAAM,CAAC,CAAC,CAAC;IAChD,OAAO,MAAM,IAAI,QAAQ,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC;AAC3C,CAAC"}
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@delphington/persistent-buckets",
3
+ "version": "0.1.0",
4
+ "description": "Versioned localStorage buckets with shared Vue state",
5
+ "license": "MIT",
6
+ "author": "Delph",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/Delph/persistent-buckets.git"
10
+ },
11
+ "homepage": "https://github.com/Delph/persistent-buckets#readme",
12
+ "keywords": [
13
+ "localstorage",
14
+ "persistence",
15
+ "vue",
16
+ "typescript"
17
+ ],
18
+ "type": "module",
19
+ "private": false,
20
+ "sideEffects": false,
21
+ "publishConfig": {
22
+ "access": "public"
23
+ },
24
+ "packageManager": "bun@1.2.12",
25
+ "files": [
26
+ "dist",
27
+ "LICENSE",
28
+ "README.md"
29
+ ],
30
+ "main": "./dist/index.js",
31
+ "types": "./dist/index.d.ts",
32
+ "exports": {
33
+ ".": {
34
+ "types": "./dist/index.d.ts",
35
+ "import": "./dist/index.js"
36
+ }
37
+ },
38
+ "scripts": {
39
+ "build": "tsc -p tsconfig.build.json",
40
+ "prepack": "bun run check",
41
+ "check": "bun run format:check && bun run typecheck && bun run test && bun run build",
42
+ "format": "prettier . --write",
43
+ "format:check": "prettier . --check",
44
+ "test": "vitest run",
45
+ "test:watch": "vitest",
46
+ "typecheck": "tsc --noEmit"
47
+ },
48
+ "peerDependencies": {
49
+ "vue": "^3.5.0"
50
+ },
51
+ "devDependencies": {
52
+ "prettier": "^3.9.6",
53
+ "typescript": "~6.0.2",
54
+ "vitest": "^4.1.11",
55
+ "vue": "^3.5.40"
56
+ }
57
+ }