seitu 0.14.1 → 0.15.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.
Files changed (33) hide show
  1. package/dist/core.d.mts +1 -1
  2. package/dist/core.mjs +1 -1
  3. package/dist/{index-Dzu3WPwr.d.mts → index-DwN8Up-P.d.mts} +1 -1
  4. package/dist/react.d.mts +2 -2
  5. package/dist/utils.d.mts +1 -24
  6. package/dist/utils.mjs +1 -1
  7. package/dist/validation-BRHMV4Ky.mjs +9 -0
  8. package/dist/vue.d.mts +1 -1
  9. package/dist/web.d.mts +181 -72
  10. package/dist/web.mjs +227 -3
  11. package/package.json +14 -11
  12. package/skills/Subscription-react/SKILL.md +34 -0
  13. package/skills/createComputed/SKILL.md +50 -0
  14. package/skills/createDebounced/SKILL.md +37 -0
  15. package/skills/createDebouncedFn/SKILL.md +39 -0
  16. package/skills/createIndexedDbStorage/SKILL.md +63 -0
  17. package/skills/createIsOnline/SKILL.md +30 -0
  18. package/skills/createMediaQuery/SKILL.md +43 -0
  19. package/skills/createReadableSubscription/SKILL.md +44 -0
  20. package/skills/createSchemaStore/SKILL.md +42 -0
  21. package/skills/createScrollState/SKILL.md +63 -0
  22. package/skills/createStore/SKILL.md +48 -0
  23. package/skills/createSubscription/SKILL.md +47 -0
  24. package/skills/createThrottled/SKILL.md +37 -0
  25. package/skills/createThrottledFn/SKILL.md +40 -0
  26. package/skills/createWebStorage/SKILL.md +64 -0
  27. package/skills/createWebStorageValue/SKILL.md +60 -0
  28. package/skills/seitu-overview/SKILL.md +148 -0
  29. package/skills/useSubscription-react/SKILL.md +71 -0
  30. package/skills/useSubscription-vue/SKILL.md +75 -0
  31. package/dist/validation-B8XxyKZ0.mjs +0 -32
  32. /package/dist/{core-CwV8yAwb.mjs → core-Vbl0ORaj.mjs} +0 -0
  33. /package/dist/{validate-IWiN_jFZ.d.mts → validate-BYHKInzu.d.mts} +0 -0
package/dist/core.d.mts CHANGED
@@ -1,2 +1,2 @@
1
- import { S as createSubscription, _ as Readable, a as Store, b as Writable, c as SchemaStoreOptions, d as createDebouncedFn, f as Debounced, g as Clearable, h as createComputed, i as createThrottled, l as createSchemaStore, m as Computed, n as createThrottledFn, o as createStore, p as createDebounced, r as Throttled, s as SchemaStore, t as ThrottledFn, u as DebouncedFn, v as Subscribable, x as createReadableSubscription, y as SubscribeOptions } from "./index-Dzu3WPwr.mjs";
1
+ import { S as createSubscription, _ as Readable, a as Store, b as Writable, c as SchemaStoreOptions, d as createDebouncedFn, f as Debounced, g as Clearable, h as createComputed, i as createThrottled, l as createSchemaStore, m as Computed, n as createThrottledFn, o as createStore, p as createDebounced, r as Throttled, s as SchemaStore, t as ThrottledFn, u as DebouncedFn, v as Subscribable, x as createReadableSubscription, y as SubscribeOptions } from "./index-DwN8Up-P.mjs";
2
2
  export { Clearable, Computed, Debounced, DebouncedFn, Readable, SchemaStore, SchemaStoreOptions, Store, Subscribable, SubscribeOptions, Throttled, ThrottledFn, Writable, createComputed, createDebounced, createDebouncedFn, createReadableSubscription, createSchemaStore, createStore, createSubscription, createThrottled, createThrottledFn };
package/dist/core.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { c as createDebounced, d as createSubscription, i as createSchemaStore, l as createComputed, n as createThrottled, r as createStore, s as createDebouncedFn, t as createThrottledFn, u as createReadableSubscription } from "./core-CwV8yAwb.mjs";
1
+ import { c as createDebounced, d as createSubscription, i as createSchemaStore, l as createComputed, n as createThrottled, r as createStore, s as createDebouncedFn, t as createThrottledFn, u as createReadableSubscription } from "./core-Vbl0ORaj.mjs";
2
2
  export { createComputed, createDebounced, createDebouncedFn, createReadableSubscription, createSchemaStore, createStore, createSubscription, createThrottled, createThrottledFn };
@@ -1,4 +1,4 @@
1
- import { i as StandardSchemaV1, r as ValidationSchemaOutput, t as ValidationSchemaErrorProps } from "./validate-IWiN_jFZ.mjs";
1
+ import { i as StandardSchemaV1, r as ValidationSchemaOutput, t as ValidationSchemaErrorProps } from "./validate-BYHKInzu.mjs";
2
2
 
3
3
  //#region src/core/subscription.d.ts
4
4
  interface SubscribeOptions {
package/dist/react.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { _ as Readable, v as Subscribable } from "./index-Dzu3WPwr.mjs";
1
+ import { _ as Readable, v as Subscribable } from "./index-DwN8Up-P.mjs";
2
2
  import * as React$1 from "react";
3
3
 
4
4
  //#region src/react/hooks.d.ts
@@ -159,6 +159,6 @@ declare function Subscription<S extends Subscribable<any> & Readable<any>, R = S
159
159
  value,
160
160
  selector,
161
161
  children
162
- }: SubscriptionProps<S, R>): React$1.ReactNode;
162
+ }: SubscriptionProps<S, R>): import("react").ReactNode;
163
163
  //#endregion
164
164
  export { Subscription, SubscriptionProps, UseSubscriptionOptions, useSubscription };
package/dist/utils.d.mts CHANGED
@@ -1,29 +1,6 @@
1
- import { t as ValidationSchemaErrorProps } from "./validate-IWiN_jFZ.mjs";
1
+ import { t as ValidationSchemaErrorProps } from "./validate-BYHKInzu.mjs";
2
2
 
3
3
  //#region src/utils/validation.d.ts
4
- /**
5
- * Repair broken web storage value object if value was object but not all keys are in the default value object.
6
- *
7
- * @returns A new object with the existing keys in the value object that are not in the default value object.
8
- *
9
- * @example
10
- * ```ts
11
- * import { createWebStorageValue } from 'seitu/web'
12
- * import { repairValueObjectWithDefault } from 'seitu/utils'
13
- * import * as z from 'zod'
14
- *
15
- * const value = createWebStorageValue({
16
- * type: 'localStorage',
17
- * schema: z.object({ a: z.number(), b: z.string() }),
18
- * key: 'storage-key',
19
- * defaultValue: { a: 0, b: 'default' },
20
- * onValidationError: repairValueObjectWithDefault,
21
- * })
22
- * value.get() // { a: 0, b: 'default' }
23
- * window.localStorage.setItem('storage-key', JSON.stringify({ a: 1 }))
24
- * value.get() // { a: 1, b: 'default' }
25
- * ```
26
- */
27
4
  declare function repairValueObjectWithDefault<O extends Record<string, unknown>>(props: ValidationSchemaErrorProps<O>): O & {
28
5
  [k: string]: any;
29
6
  };
package/dist/utils.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { t as repairValueObjectWithDefault } from "./validation-B8XxyKZ0.mjs";
1
+ import { t as repairValueObjectWithDefault } from "./validation-BRHMV4Ky.mjs";
2
2
  export { repairValueObjectWithDefault };
@@ -0,0 +1,9 @@
1
+ //#region src/utils/validation.ts
2
+ function repairValueObjectWithDefault(props) {
3
+ return {
4
+ ...props.defaultValue,
5
+ ...typeof props.value === "object" && props.value !== null ? Object.fromEntries(Object.entries(props.value).filter(([key, val]) => key in props.defaultValue && typeof val === typeof props.defaultValue[key])) : {}
6
+ };
7
+ }
8
+ //#endregion
9
+ export { repairValueObjectWithDefault as t };
package/dist/vue.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { _ as Readable, v as Subscribable } from "./index-Dzu3WPwr.mjs";
1
+ import { _ as Readable, v as Subscribable } from "./index-DwN8Up-P.mjs";
2
2
  import { MaybeRefOrGetter, ShallowRef } from "vue";
3
3
 
4
4
  //#region src/vue/composables.d.ts
package/dist/web.d.mts CHANGED
@@ -1,6 +1,184 @@
1
- import { _ as Readable, b as Writable, g as Clearable, v as Subscribable } from "./index-Dzu3WPwr.mjs";
2
- import { i as StandardSchemaV1, n as ValidationSchemaObjectErrorProps, t as ValidationSchemaErrorProps } from "./validate-IWiN_jFZ.mjs";
1
+ import { _ as Readable, b as Writable, g as Clearable, v as Subscribable } from "./index-DwN8Up-P.mjs";
2
+ import { i as StandardSchemaV1, n as ValidationSchemaObjectErrorProps, t as ValidationSchemaErrorProps } from "./validate-BYHKInzu.mjs";
3
3
 
4
+ //#region src/utils.d.ts
5
+ type Simplify<T> = { [KeyType in keyof T]: T[KeyType] } & {};
6
+ //#endregion
7
+ //#region src/web/web-storage.d.ts
8
+ type WebStorageInput = Record<string, StandardSchemaV1<unknown, unknown>>;
9
+ type WebStorageOutput<S extends WebStorageInput> = Simplify<{ [K in keyof S]: StandardSchemaV1.InferOutput<S[K]> }>;
10
+ interface WebStorageOptions<S extends WebStorageInput> {
11
+ schemas: S;
12
+ defaultValues: WebStorageOutput<S>;
13
+ type: 'localStorage' | 'sessionStorage';
14
+ keyTransform?: (key: keyof S) => string;
15
+ onValidationError?: (props: ValidationSchemaObjectErrorProps<WebStorageOutput<S>>) => void | StandardSchemaV1.InferOutput<S[keyof S]>;
16
+ }
17
+ interface WebStorage<O extends Record<string, unknown>> extends Subscribable<O>, Readable<O>, Writable<Partial<O>, O>, Clearable {
18
+ '~': {
19
+ getDefaultValue: <K extends keyof O>(key: K) => O[K];
20
+ type: 'localStorage' | 'sessionStorage';
21
+ } & Subscribable<O>['~'];
22
+ }
23
+ /**
24
+ * Creates a reactive handle for a localStorage or sessionStorage instance.
25
+ *
26
+ * @example Vanilla
27
+ * ```ts twoslash title="session-storage.ts"
28
+ * import { createWebStorage } from 'seitu/web'
29
+ * import * as z from 'zod'
30
+ *
31
+ * const sessionStorage = createWebStorage({
32
+ * type: 'sessionStorage',
33
+ * schemas: {
34
+ * token: z.string().nullable(),
35
+ * preferences: z.object({ theme: z.enum(['light', 'dark']) }),
36
+ * },
37
+ * defaultValues: { token: null, preferences: { theme: 'light' } },
38
+ * })
39
+ *
40
+ * sessionStorage.get() // { token: null, preferences: { theme: 'light' } }
41
+ * sessionStorage.set({ token: 'abc' })
42
+ * sessionStorage.get() // { token: 'abc', preferences: { theme: 'light' } }
43
+ * sessionStorage.subscribe(console.log)
44
+ * ```
45
+ *
46
+ * @example React
47
+ * ```tsx twoslash title="page.tsx"
48
+ * 'use client'
49
+ *
50
+ * import { createWebStorage } from 'seitu/web'
51
+ * import { useSubscription } from 'seitu/react'
52
+ * import * as z from 'zod'
53
+ *
54
+ * const sessionStorage = createWebStorage({
55
+ * type: 'sessionStorage',
56
+ * schemas: { count: z.number(), name: z.string() },
57
+ * defaultValues: { count: 0, name: '' },
58
+ * })
59
+ *
60
+ * export default function Page() {
61
+ * const value = useSubscription(sessionStorage)
62
+ * return (
63
+ * <div>
64
+ * <span>{value.count}</span>
65
+ * <span>{value.name}</span>
66
+ * </div>
67
+ * )
68
+ * }
69
+ * ```
70
+ */
71
+ declare function createWebStorage<S extends WebStorageInput>(options: WebStorageOptions<S>): WebStorage<WebStorageOutput<S>>;
72
+ //#endregion
73
+ //#region src/web/indexed-db-storage.d.ts
74
+ interface IndexedDbStorageOptions<S extends WebStorageInput> {
75
+ schemas: S;
76
+ defaultValues: WebStorageOutput<S>;
77
+ /**
78
+ * Name of the IndexedDB database to open.
79
+ */
80
+ databaseName: string;
81
+ /**
82
+ * Name of the object store used to persist values.
83
+ *
84
+ * @default 'seitu'
85
+ */
86
+ storeName?: string;
87
+ /**
88
+ * Version of the IndexedDB database. Leave unset to let the storage manage
89
+ * the version automatically (it bumps the version when it needs to create
90
+ * its object store).
91
+ */
92
+ version?: number;
93
+ keyTransform?: (key: keyof S) => string;
94
+ onValidationError?: (props: ValidationSchemaObjectErrorProps<WebStorageOutput<S>>) => void | StandardSchemaV1.InferOutput<S[keyof S]>;
95
+ }
96
+ interface IndexedDbStorage<O extends Record<string, unknown>> extends Subscribable<O>, Readable<O>, Writable<Partial<O>, O>, Clearable {
97
+ /**
98
+ * Resolves once the initial value has been read from IndexedDB.
99
+ *
100
+ * Because IndexedDB is asynchronous, `get()` returns the in-memory cache
101
+ * (the default values until hydration completes). Await `ready` to read the
102
+ * persisted value. This promise never rejects — read failures fall back to
103
+ * the default values and are logged.
104
+ */
105
+ 'ready': Promise<O>;
106
+ /**
107
+ * Updates the in-memory cache synchronously (so `get()` reflects the change
108
+ * immediately) and persists to IndexedDB asynchronously. The returned promise
109
+ * resolves once persistence settles and never rejects.
110
+ */
111
+ 'set': (value: Partial<O> | ((prev: O) => Partial<O>)) => Promise<void>;
112
+ /**
113
+ * Resets the cache to the default values synchronously and removes the managed
114
+ * keys from IndexedDB asynchronously. The returned promise resolves once
115
+ * persistence settles and never rejects.
116
+ */
117
+ 'clear': () => Promise<void>;
118
+ '~': {
119
+ getDefaultValue: <K extends keyof O>(key: K) => O[K];
120
+ databaseName: string;
121
+ storeName: string;
122
+ } & Subscribable<O>['~'];
123
+ }
124
+ /**
125
+ * Creates a reactive handle for an IndexedDB object store.
126
+ *
127
+ * IndexedDB is asynchronous, so the handle keeps an in-memory cache that
128
+ * `get()` reads synchronously. The cache is hydrated from IndexedDB on
129
+ * creation (await `ready` to know when), and changes made via `set()` /
130
+ * `clear()` are persisted asynchronously. When `BroadcastChannel` is
131
+ * available, changes are synced across tabs while there is at least one
132
+ * subscriber.
133
+ *
134
+ * @example Vanilla
135
+ * ```ts twoslash title="settings-storage.ts"
136
+ * import { createIndexedDbStorage } from 'seitu/web'
137
+ * import * as z from 'zod'
138
+ *
139
+ * const settingsStorage = createIndexedDbStorage({
140
+ * databaseName: 'app',
141
+ * schemas: {
142
+ * token: z.string().nullable(),
143
+ * preferences: z.object({ theme: z.enum(['light', 'dark']) }),
144
+ * },
145
+ * defaultValues: { token: null, preferences: { theme: 'light' } },
146
+ * })
147
+ *
148
+ * settingsStorage.get() // { token: null, preferences: { theme: 'light' } }
149
+ * await settingsStorage.ready // value hydrated from IndexedDB
150
+ * await settingsStorage.set({ token: 'abc' })
151
+ * settingsStorage.get() // { token: 'abc', preferences: { theme: 'light' } }
152
+ * settingsStorage.subscribe(console.log)
153
+ * ```
154
+ *
155
+ * @example React
156
+ * ```tsx twoslash title="page.tsx"
157
+ * 'use client'
158
+ *
159
+ * import { createIndexedDbStorage } from 'seitu/web'
160
+ * import { useSubscription } from 'seitu/react'
161
+ * import * as z from 'zod'
162
+ *
163
+ * const settingsStorage = createIndexedDbStorage({
164
+ * databaseName: 'app',
165
+ * schemas: { count: z.number(), name: z.string() },
166
+ * defaultValues: { count: 0, name: '' },
167
+ * })
168
+ *
169
+ * export default function Page() {
170
+ * const value = useSubscription(settingsStorage)
171
+ * return (
172
+ * <div>
173
+ * <span>{value.count}</span>
174
+ * <span>{value.name}</span>
175
+ * </div>
176
+ * )
177
+ * }
178
+ * ```
179
+ */
180
+ declare function createIndexedDbStorage<S extends WebStorageInput>(options: IndexedDbStorageOptions<S>): IndexedDbStorage<WebStorageOutput<S>>;
181
+ //#endregion
4
182
  //#region src/web/is-online.d.ts
5
183
  interface IsOnline extends Subscribable<boolean>, Readable<boolean> {}
6
184
  /**
@@ -221,75 +399,6 @@ interface ScrollStateOptions {
221
399
  */
222
400
  declare function createScrollState(options: ScrollStateOptions): ScrollState;
223
401
  //#endregion
224
- //#region src/utils.d.ts
225
- type Simplify<T> = { [KeyType in keyof T]: T[KeyType] } & {};
226
- //#endregion
227
- //#region src/web/web-storage.d.ts
228
- type WebStorageInput = Record<string, StandardSchemaV1<unknown, unknown>>;
229
- type WebStorageOutput<S extends WebStorageInput> = Simplify<{ [K in keyof S]: StandardSchemaV1.InferOutput<S[K]> }>;
230
- interface WebStorageOptions<S extends WebStorageInput> {
231
- schemas: S;
232
- defaultValues: WebStorageOutput<S>;
233
- type: 'localStorage' | 'sessionStorage';
234
- keyTransform?: (key: keyof S) => string;
235
- onValidationError?: (props: ValidationSchemaObjectErrorProps<WebStorageOutput<S>>) => void | StandardSchemaV1.InferOutput<S[keyof S]>;
236
- }
237
- interface WebStorage<O extends Record<string, unknown>> extends Subscribable<O>, Readable<O>, Writable<Partial<O>, O>, Clearable {
238
- '~': {
239
- getDefaultValue: <K extends keyof O>(key: K) => O[K];
240
- type: 'localStorage' | 'sessionStorage';
241
- } & Subscribable<O>['~'];
242
- }
243
- /**
244
- * Creates a reactive handle for a localStorage or sessionStorage instance.
245
- *
246
- * @example Vanilla
247
- * ```ts twoslash title="session-storage.ts"
248
- * import { createWebStorage } from 'seitu/web'
249
- * import * as z from 'zod'
250
- *
251
- * const sessionStorage = createWebStorage({
252
- * type: 'sessionStorage',
253
- * schemas: {
254
- * token: z.string().nullable(),
255
- * preferences: z.object({ theme: z.enum(['light', 'dark']) }),
256
- * },
257
- * defaultValues: { token: null, preferences: { theme: 'light' } },
258
- * })
259
- *
260
- * sessionStorage.get() // { token: null, preferences: { theme: 'light' } }
261
- * sessionStorage.set({ token: 'abc' })
262
- * sessionStorage.get() // { token: 'abc', preferences: { theme: 'light' } }
263
- * sessionStorage.subscribe(console.log)
264
- * ```
265
- *
266
- * @example React
267
- * ```tsx twoslash title="page.tsx"
268
- * 'use client'
269
- *
270
- * import { createWebStorage } from 'seitu/web'
271
- * import { useSubscription } from 'seitu/react'
272
- * import * as z from 'zod'
273
- *
274
- * const sessionStorage = createWebStorage({
275
- * type: 'sessionStorage',
276
- * schemas: { count: z.number(), name: z.string() },
277
- * defaultValues: { count: 0, name: '' },
278
- * })
279
- *
280
- * export default function Page() {
281
- * const value = useSubscription(sessionStorage)
282
- * return (
283
- * <div>
284
- * <span>{value.count}</span>
285
- * <span>{value.name}</span>
286
- * </div>
287
- * )
288
- * }
289
- * ```
290
- */
291
- declare function createWebStorage<S extends WebStorageInput>(options: WebStorageOptions<S>): WebStorage<WebStorageOutput<S>>;
292
- //#endregion
293
402
  //#region src/web/web-storage-value.d.ts
294
403
  interface WebStorageValue<V> extends Subscribable<V>, Readable<V>, Writable<V>, Clearable {}
295
404
  interface WebStorageValueOptionsWithStorage<Storage extends WebStorage<any>, K extends keyof Storage['~']['output']> {
@@ -380,4 +489,4 @@ interface WebStorageValueOptionsWithSchema<S extends StandardSchemaV1<unknown>>
380
489
  declare function createWebStorageValue<Storage extends WebStorage<any>, K extends keyof Storage['~']['output']>(options: WebStorageValueOptionsWithStorage<Storage, K>): WebStorageValue<Storage['~']['output'][K]>;
381
490
  declare function createWebStorageValue<S extends StandardSchemaV1<unknown>>(options: WebStorageValueOptionsWithSchema<S>): WebStorageValue<StandardSchemaV1.InferOutput<S>>;
382
491
  //#endregion
383
- export { IsOnline, MediaQuery, MediaQueryOptions, MediaQueryType, ScrollDirection, ScrollState, ScrollStateEdge, ScrollStateOptions, ScrollStateThreshold, ScrollStateValue, WebStorage, WebStorageInput, WebStorageOptions, WebStorageOutput, WebStorageValue, WebStorageValueOptionsWithSchema, WebStorageValueOptionsWithStorage, createIsOnline, createMediaQuery, createScrollState, createWebStorage, createWebStorageValue };
492
+ export { IndexedDbStorage, IndexedDbStorageOptions, IsOnline, MediaQuery, MediaQueryOptions, MediaQueryType, ScrollDirection, ScrollState, ScrollStateEdge, ScrollStateOptions, ScrollStateThreshold, ScrollStateValue, WebStorage, WebStorageInput, WebStorageOptions, WebStorageOutput, WebStorageValue, WebStorageValueOptionsWithSchema, WebStorageValueOptionsWithStorage, createIndexedDbStorage, createIsOnline, createMediaQuery, createScrollState, createWebStorage, createWebStorageValue };
package/dist/web.mjs CHANGED
@@ -1,5 +1,229 @@
1
- import { a as validateSchema, d as createSubscription, o as tryParseJson, u as createReadableSubscription } from "./core-CwV8yAwb.mjs";
2
- import { t as repairValueObjectWithDefault } from "./validation-B8XxyKZ0.mjs";
1
+ import { a as validateSchema, d as createSubscription, o as tryParseJson, u as createReadableSubscription } from "./core-Vbl0ORaj.mjs";
2
+ import { t as repairValueObjectWithDefault } from "./validation-BRHMV4Ky.mjs";
3
+ import { deepEqual } from "fast-equals";
4
+ //#region src/web/indexed-db-storage.ts
5
+ function requestToPromise(request) {
6
+ return new Promise((resolve, reject) => {
7
+ request.onsuccess = () => resolve(request.result);
8
+ request.onerror = () => reject(request.error);
9
+ });
10
+ }
11
+ function transactionDone(transaction) {
12
+ return new Promise((resolve, reject) => {
13
+ transaction.oncomplete = () => resolve();
14
+ transaction.onerror = () => reject(transaction.error);
15
+ transaction.onabort = () => reject(transaction.error);
16
+ });
17
+ }
18
+ function openRequest(name, version, storeName) {
19
+ return new Promise((resolve, reject) => {
20
+ const request = version === void 0 ? indexedDB.open(name) : indexedDB.open(name, version);
21
+ request.onupgradeneeded = () => {
22
+ const db = request.result;
23
+ if (!db.objectStoreNames.contains(storeName)) db.createObjectStore(storeName);
24
+ };
25
+ request.onsuccess = () => resolve(request.result);
26
+ request.onerror = () => reject(request.error);
27
+ });
28
+ }
29
+ async function openDatabase(name, storeName, version) {
30
+ const db = await openRequest(name, version, storeName);
31
+ if (db.objectStoreNames.contains(storeName)) return db;
32
+ if (version !== void 0) return db;
33
+ const nextVersion = db.version + 1;
34
+ db.close();
35
+ return openRequest(name, nextVersion, storeName);
36
+ }
37
+ /**
38
+ * Creates a reactive handle for an IndexedDB object store.
39
+ *
40
+ * IndexedDB is asynchronous, so the handle keeps an in-memory cache that
41
+ * `get()` reads synchronously. The cache is hydrated from IndexedDB on
42
+ * creation (await `ready` to know when), and changes made via `set()` /
43
+ * `clear()` are persisted asynchronously. When `BroadcastChannel` is
44
+ * available, changes are synced across tabs while there is at least one
45
+ * subscriber.
46
+ *
47
+ * @example Vanilla
48
+ * ```ts twoslash title="settings-storage.ts"
49
+ * import { createIndexedDbStorage } from 'seitu/web'
50
+ * import * as z from 'zod'
51
+ *
52
+ * const settingsStorage = createIndexedDbStorage({
53
+ * databaseName: 'app',
54
+ * schemas: {
55
+ * token: z.string().nullable(),
56
+ * preferences: z.object({ theme: z.enum(['light', 'dark']) }),
57
+ * },
58
+ * defaultValues: { token: null, preferences: { theme: 'light' } },
59
+ * })
60
+ *
61
+ * settingsStorage.get() // { token: null, preferences: { theme: 'light' } }
62
+ * await settingsStorage.ready // value hydrated from IndexedDB
63
+ * await settingsStorage.set({ token: 'abc' })
64
+ * settingsStorage.get() // { token: 'abc', preferences: { theme: 'light' } }
65
+ * settingsStorage.subscribe(console.log)
66
+ * ```
67
+ *
68
+ * @example React
69
+ * ```tsx twoslash title="page.tsx"
70
+ * 'use client'
71
+ *
72
+ * import { createIndexedDbStorage } from 'seitu/web'
73
+ * import { useSubscription } from 'seitu/react'
74
+ * import * as z from 'zod'
75
+ *
76
+ * const settingsStorage = createIndexedDbStorage({
77
+ * databaseName: 'app',
78
+ * schemas: { count: z.number(), name: z.string() },
79
+ * defaultValues: { count: 0, name: '' },
80
+ * })
81
+ *
82
+ * export default function Page() {
83
+ * const value = useSubscription(settingsStorage)
84
+ * return (
85
+ * <div>
86
+ * <span>{value.count}</span>
87
+ * <span>{value.name}</span>
88
+ * </div>
89
+ * )
90
+ * }
91
+ * ```
92
+ */
93
+ function createIndexedDbStorage(options) {
94
+ const defaultValues = { ...options.defaultValues };
95
+ const storeName = options.storeName ?? "seitu";
96
+ const keys = Object.keys(options.defaultValues);
97
+ const isSupported = typeof indexedDB !== "undefined";
98
+ const channelName = `seitu:indexed-db:${options.databaseName}:${storeName}`;
99
+ const label = `createIndexedDbStorage:${options.databaseName}/${storeName}`;
100
+ const resolveKey = (key) => String(options.keyTransform ? options.keyTransform(key) : key);
101
+ let cache = { ...options.defaultValues };
102
+ let writeVersion = 0;
103
+ const keyWriteVersion = /* @__PURE__ */ new Map();
104
+ let hydrateSeq = 0;
105
+ const markWritten = (written) => {
106
+ writeVersion++;
107
+ for (const key of written) keyWriteVersion.set(key, writeVersion);
108
+ };
109
+ let channel;
110
+ let triggerHydrate = () => {};
111
+ const broadcast = () => {
112
+ if (typeof BroadcastChannel === "undefined") return;
113
+ if (channel) {
114
+ channel.postMessage(null);
115
+ return;
116
+ }
117
+ const ephemeral = new BroadcastChannel(channelName);
118
+ ephemeral.postMessage(null);
119
+ ephemeral.close();
120
+ };
121
+ let dbPromise;
122
+ const getDatabase = () => {
123
+ if (!dbPromise) dbPromise = openDatabase(options.databaseName, storeName, options.version).then((db) => {
124
+ db.onversionchange = () => {
125
+ db.close();
126
+ dbPromise = void 0;
127
+ };
128
+ return db;
129
+ }).catch((error) => {
130
+ dbPromise = void 0;
131
+ throw error;
132
+ });
133
+ return dbPromise;
134
+ };
135
+ const { subscribe, notify } = createSubscription({ onFirstSubscribe: () => {
136
+ triggerHydrate();
137
+ if (typeof BroadcastChannel === "undefined") return;
138
+ channel = new BroadcastChannel(channelName);
139
+ channel.onmessage = () => triggerHydrate();
140
+ return () => {
141
+ channel?.close();
142
+ channel = void 0;
143
+ };
144
+ } });
145
+ const validateValue = (key, raw) => {
146
+ if (raw === void 0) return options.defaultValues[key];
147
+ return validateSchema(options.schemas[key], raw, {
148
+ defaultValue: options.defaultValues[key],
149
+ label: `createIndexedDbStorage:${String(key)}`,
150
+ onError: options.onValidationError ? (issues, parsed) => options.onValidationError({
151
+ issues: [...issues],
152
+ key,
153
+ value: parsed,
154
+ defaultValue: options.defaultValues[key]
155
+ }) : void 0
156
+ });
157
+ };
158
+ const hydrate = async () => {
159
+ if (!isSupported) return cache;
160
+ const seq = ++hydrateSeq;
161
+ const startVersion = writeVersion;
162
+ const store = (await getDatabase()).transaction(storeName, "readonly").objectStore(storeName);
163
+ const persisted = {};
164
+ await Promise.all(keys.map(async (key) => {
165
+ persisted[key] = validateValue(key, await requestToPromise(store.get(resolveKey(key))));
166
+ }));
167
+ if (seq !== hydrateSeq) return cache;
168
+ const next = {};
169
+ for (const key of keys) next[key] = (keyWriteVersion.get(key) ?? 0) > startVersion ? cache[key] : persisted[key];
170
+ if (!deepEqual(cache, next)) {
171
+ cache = next;
172
+ notify();
173
+ }
174
+ return cache;
175
+ };
176
+ const safeHydrate = () => hydrate().catch((error) => {
177
+ console.warn(`[${label}] Failed to read from IndexedDB, using cached values.`, error);
178
+ return cache;
179
+ });
180
+ triggerHydrate = () => void safeHydrate();
181
+ const ready = safeHydrate();
182
+ const get = () => cache;
183
+ const readable = createReadableSubscription(get, subscribe, notify);
184
+ const persist = async (mutate) => {
185
+ if (!isSupported) return;
186
+ try {
187
+ const transaction = (await getDatabase()).transaction(storeName, "readwrite");
188
+ mutate(transaction.objectStore(storeName));
189
+ await transactionDone(transaction);
190
+ broadcast();
191
+ } catch (error) {
192
+ console.warn(`[${label}] Failed to write to IndexedDB.`, error);
193
+ }
194
+ };
195
+ return {
196
+ ...readable,
197
+ ready,
198
+ "set": (value) => {
199
+ const resolved = typeof value === "function" ? value(cache) : value;
200
+ markWritten(Object.keys(resolved));
201
+ cache = {
202
+ ...cache,
203
+ ...resolved
204
+ };
205
+ notify();
206
+ return persist((store) => {
207
+ for (const [key, entry] of Object.entries(resolved)) store.put(entry, resolveKey(key));
208
+ });
209
+ },
210
+ "clear": () => {
211
+ markWritten(keys);
212
+ cache = { ...options.defaultValues };
213
+ notify();
214
+ return persist((store) => {
215
+ for (const key of keys) store.delete(resolveKey(key));
216
+ });
217
+ },
218
+ "~": {
219
+ ...readable["~"],
220
+ getDefaultValue: (key) => defaultValues[key],
221
+ databaseName: options.databaseName,
222
+ storeName
223
+ }
224
+ };
225
+ }
226
+ //#endregion
3
227
  //#region src/web/is-online.ts
4
228
  /**
5
229
  * Creates a reactive handle for browser online status.
@@ -458,4 +682,4 @@ function createWebStorageValue(options) {
458
682
  };
459
683
  }
460
684
  //#endregion
461
- export { createIsOnline, createMediaQuery, createScrollState, createWebStorage, createWebStorageValue };
685
+ export { createIndexedDbStorage, createIsOnline, createMediaQuery, createScrollState, createWebStorage, createWebStorageValue };
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "seitu",
3
3
  "displayName": "Seitu",
4
4
  "type": "module",
5
- "version": "0.14.1",
5
+ "version": "0.15.0",
6
6
  "private": false,
7
7
  "author": "Valerii Strilets",
8
8
  "license": "MIT",
@@ -48,7 +48,8 @@
48
48
  "main": "./dist/core.mjs",
49
49
  "types": "./dist/core/index.d.mts",
50
50
  "files": [
51
- "dist"
51
+ "dist",
52
+ "skills"
52
53
  ],
53
54
  "engines": {
54
55
  "node": ">=22"
@@ -76,21 +77,23 @@
76
77
  "@standard-schema/spec": "^1.1.0",
77
78
  "@testing-library/jest-dom": "^6.9.1",
78
79
  "@testing-library/react": "^16.3.2",
79
- "@types/react": "^19.2.14",
80
- "happy-dom": "^20.9.0",
81
- "react-dom": "^19.2.6",
82
- "tsdown": "^0.22.0",
83
- "type-fest": "^5.6.0",
80
+ "@types/react": "^19.2.17",
81
+ "fake-indexeddb": "^6.2.5",
82
+ "happy-dom": "^20.10.2",
83
+ "react-dom": "^19.2.7",
84
+ "tsdown": "^0.22.2",
85
+ "type-fest": "^5.7.0",
84
86
  "typescript": "^6.0.3",
85
- "vite": "^8.0.13",
86
- "vitest": "^4.1.6",
87
- "vue": "^3.5.34",
87
+ "vite": "^6.4.3",
88
+ "vitest": "^3.2.6",
89
+ "vue": "^3.5.35",
88
90
  "zod": "^4.4.3"
89
91
  },
90
92
  "scripts": {
91
93
  "build": "tsdown",
92
94
  "test": "vitest run",
93
95
  "check-types": "tsc",
94
- "scripts:copy-readme": "node ./scripts/copy-readme.ts"
96
+ "scripts:copy-readme": "node ./scripts/copy-readme.ts",
97
+ "scripts:copy-skills": "node ./scripts/copy-skills.ts"
95
98
  }
96
99
  }
@@ -0,0 +1,34 @@
1
+ ---
2
+ name: Subscription-react
3
+ description: >-
4
+ Declarative React component for subscribing to Seitu reactive values via
5
+ render props. Use when you prefer a component API over the useSubscription hook.
6
+ ---
7
+
8
+ # Subscription (React component)
9
+
10
+ Declarative render-prop component from `seitu/react`. Isolates re-renders to the children function.
11
+
12
+ ```tsx
13
+ import { Subscription } from 'seitu/react'
14
+
15
+ <Subscription value={storage}>
16
+ {(value) => <div>{value.count}</div>}
17
+ </Subscription>
18
+
19
+ <Subscription value={storage} selector={v => v.count}>
20
+ {(count) => <div>{count}</div>}
21
+ </Subscription>
22
+ ```
23
+
24
+ ## Props
25
+
26
+ | Prop | Type | Description |
27
+ |------|------|-------------|
28
+ | `value` | `Subscribable & Readable` | The reactive source |
29
+ | `selector?` | `(value) => R` | Optional selector for granular updates |
30
+ | `children` | `(value) => ReactNode` | Render function |
31
+
32
+ ## Source
33
+
34
+ `seitu/src/react/components.tsx`