@rippledb/bind-tanstack 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.
@@ -0,0 +1,99 @@
1
+ import type { RippleSchema } from '@rippledb/core';
2
+ import type { DbEvent, Store } from '@rippledb/client';
3
+ import type { QueryClient } from '@tanstack/query-core';
4
+ export type ListRegistryEntry = {
5
+ /**
6
+ * The query key prefix to invalidate (e.g. ['todos'], ['todoList']).
7
+ */
8
+ queryKey: readonly unknown[];
9
+ /**
10
+ * Entity names this query depends on.
11
+ * When a DbEvent for any of these entities fires, the query is invalidated.
12
+ */
13
+ deps: readonly string[];
14
+ };
15
+ export type ListRegistry = {
16
+ entries: ListRegistryEntry[];
17
+ };
18
+ /**
19
+ * Fluent builder for defining which query keys depend on which entities.
20
+ *
21
+ * @example
22
+ * ```ts
23
+ * const registry = defineListRegistry()
24
+ * .list(['todos'], { deps: ['todos'] })
25
+ * .list(['todoWithTags'], { deps: ['todos', 'tags'] })
26
+ * .list(['dashboard'], { deps: ['todos', 'users', 'projects'] });
27
+ * ```
28
+ */
29
+ export declare function defineListRegistry(): ListRegistryBuilder;
30
+ declare class ListRegistryBuilder implements ListRegistry {
31
+ readonly entries: ListRegistryEntry[];
32
+ constructor(entries: ListRegistryEntry[]);
33
+ /**
34
+ * Register a list query key and its entity dependencies.
35
+ *
36
+ * @param queryKey - The query key prefix to invalidate
37
+ * @param opts - Configuration with `deps` array of entity names
38
+ */
39
+ list(queryKey: readonly unknown[], opts: {
40
+ deps: readonly string[];
41
+ }): ListRegistryBuilder;
42
+ }
43
+ export type WireTanstackInvalidationOptions<S extends RippleSchema = RippleSchema> = {
44
+ /**
45
+ * TanStack QueryClient instance.
46
+ */
47
+ queryClient: QueryClient;
48
+ /**
49
+ * The store to listen to for DbEvents.
50
+ * Must implement `onEvent(cb)`.
51
+ */
52
+ store?: Store<S>;
53
+ /**
54
+ * Alternative: provide onEvent callback directly (useful for testing).
55
+ */
56
+ onEvent?: (cb: (event: DbEvent<S>) => void) => () => void;
57
+ /**
58
+ * Registry mapping list query keys to their entity dependencies.
59
+ */
60
+ registry?: ListRegistry;
61
+ /**
62
+ * Debounce time in milliseconds to coalesce rapid-fire invalidations.
63
+ * Set to 0 to disable debouncing.
64
+ * @default 50
65
+ */
66
+ debounceMs?: number;
67
+ /**
68
+ * Whether to invalidate row queries ([entity, id]) directly.
69
+ * @default true
70
+ */
71
+ invalidateRows?: boolean;
72
+ };
73
+ /**
74
+ * Wire RippleDB DbEvents to TanStack Query cache invalidation.
75
+ *
76
+ * @example
77
+ * ```ts
78
+ * import { wireTanstackInvalidation, defineListRegistry } from '@rippledb/bind-tanstack';
79
+ *
80
+ * const registry = defineListRegistry()
81
+ * .list(['todos'], { deps: ['todos'] })
82
+ * .list(['dashboard'], { deps: ['todos', 'users'] });
83
+ *
84
+ * const cleanup = wireTanstackInvalidation({
85
+ * queryClient,
86
+ * store,
87
+ * registry,
88
+ * debounceMs: 50,
89
+ * });
90
+ *
91
+ * // Later: cleanup() to unsubscribe
92
+ * ```
93
+ *
94
+ * @returns Cleanup function to unsubscribe from events
95
+ */
96
+ export declare function wireTanstackInvalidation<S extends RippleSchema = RippleSchema>(opts: WireTanstackInvalidationOptions<S>): () => void;
97
+ export type { DbEvent, Store } from '@rippledb/client';
98
+ export type { QueryClient } from '@tanstack/query-core';
99
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AACnD,OAAO,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AACvD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAMxD,MAAM,MAAM,iBAAiB,GAAG;IAC9B;;OAEG;IACH,QAAQ,EAAE,SAAS,OAAO,EAAE,CAAC;IAC7B;;;OAGG;IACH,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;CACzB,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG;IACzB,OAAO,EAAE,iBAAiB,EAAE,CAAC;CAC9B,CAAC;AAEF;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,IAAI,mBAAmB,CAExD;AAED,cAAM,mBAAoB,YAAW,YAAY;aACnB,OAAO,EAAE,iBAAiB,EAAE;gBAA5B,OAAO,EAAE,iBAAiB,EAAE;IAExD;;;;;OAKG;IACH,IAAI,CACF,QAAQ,EAAE,SAAS,OAAO,EAAE,EAC5B,IAAI,EAAE;QAAE,IAAI,EAAE,SAAS,MAAM,EAAE,CAAA;KAAE,GAChC,mBAAmB;CAMvB;AAMD,MAAM,MAAM,+BAA+B,CACzC,CAAC,SAAS,YAAY,GAAG,YAAY,IACnC;IACF;;OAEG;IACH,WAAW,EAAE,WAAW,CAAC;IAEzB;;;OAGG;IACH,KAAK,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC;IAEjB;;OAEG;IACH,OAAO,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,KAAK,IAAI,KAAK,MAAM,IAAI,CAAC;IAE1D;;OAEG;IACH,QAAQ,CAAC,EAAE,YAAY,CAAC;IAExB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB;;;OAGG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,wBAAwB,CAAC,CAAC,SAAS,YAAY,GAAG,YAAY,EAC5E,IAAI,EAAE,+BAA+B,CAAC,CAAC,CAAC,GACvC,MAAM,IAAI,CA+EZ;AAMD,YAAY,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AACvD,YAAY,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,85 @@
1
+ // src/index.ts
2
+ function defineListRegistry() {
3
+ return new ListRegistryBuilder([]);
4
+ }
5
+ var ListRegistryBuilder = class _ListRegistryBuilder {
6
+ constructor(entries) {
7
+ this.entries = entries;
8
+ }
9
+ /**
10
+ * Register a list query key and its entity dependencies.
11
+ *
12
+ * @param queryKey - The query key prefix to invalidate
13
+ * @param opts - Configuration with `deps` array of entity names
14
+ */
15
+ list(queryKey, opts) {
16
+ return new _ListRegistryBuilder([
17
+ ...this.entries,
18
+ { queryKey, deps: opts.deps }
19
+ ]);
20
+ }
21
+ };
22
+ function wireTanstackInvalidation(opts) {
23
+ const {
24
+ queryClient,
25
+ store,
26
+ onEvent,
27
+ registry,
28
+ debounceMs = 50,
29
+ invalidateRows = true
30
+ } = opts;
31
+ const subscribe = onEvent ?? (store?.onEvent ? (cb) => store.onEvent(cb) : void 0);
32
+ if (!subscribe) {
33
+ throw new Error(
34
+ "wireTanstackInvalidation: either `store` (with onEvent) or `onEvent` callback is required"
35
+ );
36
+ }
37
+ let pendingEntities = /* @__PURE__ */ new Set();
38
+ let pendingRows = [];
39
+ let debounceTimeout = null;
40
+ const flush = () => {
41
+ debounceTimeout = null;
42
+ if (invalidateRows) {
43
+ for (const { entity, id } of pendingRows) {
44
+ queryClient.invalidateQueries({ queryKey: [entity, id] });
45
+ }
46
+ }
47
+ if (registry) {
48
+ for (const entry of registry.entries) {
49
+ const shouldInvalidate = entry.deps.some(
50
+ (dep) => pendingEntities.has(dep)
51
+ );
52
+ if (shouldInvalidate) {
53
+ queryClient.invalidateQueries({ queryKey: [...entry.queryKey] });
54
+ }
55
+ }
56
+ }
57
+ for (const entity of pendingEntities) {
58
+ queryClient.invalidateQueries({ queryKey: [entity] });
59
+ }
60
+ pendingEntities = /* @__PURE__ */ new Set();
61
+ pendingRows = [];
62
+ };
63
+ const handleEvent = (event) => {
64
+ pendingEntities.add(event.entity);
65
+ if (event.id) {
66
+ pendingRows.push({ entity: event.entity, id: event.id });
67
+ }
68
+ if (debounceMs > 0) {
69
+ if (debounceTimeout) clearTimeout(debounceTimeout);
70
+ debounceTimeout = setTimeout(flush, debounceMs);
71
+ } else {
72
+ flush();
73
+ }
74
+ };
75
+ const unsubscribe = subscribe(handleEvent);
76
+ return () => {
77
+ if (debounceTimeout) clearTimeout(debounceTimeout);
78
+ unsubscribe();
79
+ };
80
+ }
81
+ export {
82
+ defineListRegistry,
83
+ wireTanstackInvalidation
84
+ };
85
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import type { RippleSchema } from '@rippledb/core';\nimport type { DbEvent, Store } from '@rippledb/client';\nimport type { QueryClient } from '@tanstack/query-core';\n\n// ============================================================================\n// List Registry\n// ============================================================================\n\nexport type ListRegistryEntry = {\n /**\n * The query key prefix to invalidate (e.g. ['todos'], ['todoList']).\n */\n queryKey: readonly unknown[];\n /**\n * Entity names this query depends on.\n * When a DbEvent for any of these entities fires, the query is invalidated.\n */\n deps: readonly string[];\n};\n\nexport type ListRegistry = {\n entries: ListRegistryEntry[];\n};\n\n/**\n * Fluent builder for defining which query keys depend on which entities.\n *\n * @example\n * ```ts\n * const registry = defineListRegistry()\n * .list(['todos'], { deps: ['todos'] })\n * .list(['todoWithTags'], { deps: ['todos', 'tags'] })\n * .list(['dashboard'], { deps: ['todos', 'users', 'projects'] });\n * ```\n */\nexport function defineListRegistry(): ListRegistryBuilder {\n return new ListRegistryBuilder([]);\n}\n\nclass ListRegistryBuilder implements ListRegistry {\n constructor(public readonly entries: ListRegistryEntry[]) {}\n\n /**\n * Register a list query key and its entity dependencies.\n *\n * @param queryKey - The query key prefix to invalidate\n * @param opts - Configuration with `deps` array of entity names\n */\n list(\n queryKey: readonly unknown[],\n opts: { deps: readonly string[] },\n ): ListRegistryBuilder {\n return new ListRegistryBuilder([\n ...this.entries,\n { queryKey, deps: opts.deps },\n ]);\n }\n}\n\n// ============================================================================\n// Invalidation Wiring\n// ============================================================================\n\nexport type WireTanstackInvalidationOptions<\n S extends RippleSchema = RippleSchema,\n> = {\n /**\n * TanStack QueryClient instance.\n */\n queryClient: QueryClient;\n\n /**\n * The store to listen to for DbEvents.\n * Must implement `onEvent(cb)`.\n */\n store?: Store<S>;\n\n /**\n * Alternative: provide onEvent callback directly (useful for testing).\n */\n onEvent?: (cb: (event: DbEvent<S>) => void) => () => void;\n\n /**\n * Registry mapping list query keys to their entity dependencies.\n */\n registry?: ListRegistry;\n\n /**\n * Debounce time in milliseconds to coalesce rapid-fire invalidations.\n * Set to 0 to disable debouncing.\n * @default 50\n */\n debounceMs?: number;\n\n /**\n * Whether to invalidate row queries ([entity, id]) directly.\n * @default true\n */\n invalidateRows?: boolean;\n};\n\n/**\n * Wire RippleDB DbEvents to TanStack Query cache invalidation.\n *\n * @example\n * ```ts\n * import { wireTanstackInvalidation, defineListRegistry } from '@rippledb/bind-tanstack';\n *\n * const registry = defineListRegistry()\n * .list(['todos'], { deps: ['todos'] })\n * .list(['dashboard'], { deps: ['todos', 'users'] });\n *\n * const cleanup = wireTanstackInvalidation({\n * queryClient,\n * store,\n * registry,\n * debounceMs: 50,\n * });\n *\n * // Later: cleanup() to unsubscribe\n * ```\n *\n * @returns Cleanup function to unsubscribe from events\n */\nexport function wireTanstackInvalidation<S extends RippleSchema = RippleSchema>(\n opts: WireTanstackInvalidationOptions<S>,\n): () => void {\n const {\n queryClient,\n store,\n onEvent,\n registry,\n debounceMs = 50,\n invalidateRows = true,\n } = opts;\n\n // Get the event subscription function (bind to store if using store.onEvent)\n const subscribe = onEvent ?? (store?.onEvent ? (cb: (event: DbEvent<S>) => void) => store.onEvent!(cb) : undefined);\n if (!subscribe) {\n throw new Error(\n 'wireTanstackInvalidation: either `store` (with onEvent) or `onEvent` callback is required',\n );\n }\n\n // Track pending invalidations for debouncing\n let pendingEntities = new Set<string>();\n let pendingRows: Array<{ entity: string; id: string }> = [];\n let debounceTimeout: ReturnType<typeof setTimeout> | null = null;\n\n const flush = () => {\n debounceTimeout = null;\n\n // Invalidate row queries\n if (invalidateRows) {\n for (const { entity, id } of pendingRows) {\n queryClient.invalidateQueries({ queryKey: [entity, id] });\n }\n }\n\n // Invalidate list queries based on registry\n if (registry) {\n for (const entry of registry.entries) {\n const shouldInvalidate = entry.deps.some((dep) =>\n pendingEntities.has(dep),\n );\n if (shouldInvalidate) {\n queryClient.invalidateQueries({ queryKey: [...entry.queryKey] });\n }\n }\n }\n\n // Also invalidate entity-level queries (e.g. ['todos'])\n for (const entity of pendingEntities) {\n queryClient.invalidateQueries({ queryKey: [entity] });\n }\n\n // Clear pending\n pendingEntities = new Set();\n pendingRows = [];\n };\n\n const handleEvent = (event: DbEvent<S>) => {\n pendingEntities.add(event.entity as string);\n\n if (event.id) {\n pendingRows.push({ entity: event.entity as string, id: event.id });\n }\n\n // Schedule flush\n if (debounceMs > 0) {\n if (debounceTimeout) clearTimeout(debounceTimeout);\n debounceTimeout = setTimeout(flush, debounceMs);\n } else {\n flush();\n }\n };\n\n // Subscribe to events\n const unsubscribe = subscribe(handleEvent);\n\n // Return cleanup function\n return () => {\n if (debounceTimeout) clearTimeout(debounceTimeout);\n unsubscribe();\n };\n}\n\n// ============================================================================\n// Re-exports for convenience\n// ============================================================================\n\nexport type { DbEvent, Store } from '@rippledb/client';\nexport type { QueryClient } from '@tanstack/query-core';\n"],"mappings":";AAmCO,SAAS,qBAA0C;AACxD,SAAO,IAAI,oBAAoB,CAAC,CAAC;AACnC;AAEA,IAAM,sBAAN,MAAM,qBAA4C;AAAA,EAChD,YAA4B,SAA8B;AAA9B;AAAA,EAA+B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQ3D,KACE,UACA,MACqB;AACrB,WAAO,IAAI,qBAAoB;AAAA,MAC7B,GAAG,KAAK;AAAA,MACR,EAAE,UAAU,MAAM,KAAK,KAAK;AAAA,IAC9B,CAAC;AAAA,EACH;AACF;AAmEO,SAAS,yBACd,MACY;AACZ,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,aAAa;AAAA,IACb,iBAAiB;AAAA,EACnB,IAAI;AAGJ,QAAM,YAAY,YAAY,OAAO,UAAU,CAAC,OAAoC,MAAM,QAAS,EAAE,IAAI;AACzG,MAAI,CAAC,WAAW;AACd,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AAGA,MAAI,kBAAkB,oBAAI,IAAY;AACtC,MAAI,cAAqD,CAAC;AAC1D,MAAI,kBAAwD;AAE5D,QAAM,QAAQ,MAAM;AAClB,sBAAkB;AAGlB,QAAI,gBAAgB;AAClB,iBAAW,EAAE,QAAQ,GAAG,KAAK,aAAa;AACxC,oBAAY,kBAAkB,EAAE,UAAU,CAAC,QAAQ,EAAE,EAAE,CAAC;AAAA,MAC1D;AAAA,IACF;AAGA,QAAI,UAAU;AACZ,iBAAW,SAAS,SAAS,SAAS;AACpC,cAAM,mBAAmB,MAAM,KAAK;AAAA,UAAK,CAAC,QACxC,gBAAgB,IAAI,GAAG;AAAA,QACzB;AACA,YAAI,kBAAkB;AACpB,sBAAY,kBAAkB,EAAE,UAAU,CAAC,GAAG,MAAM,QAAQ,EAAE,CAAC;AAAA,QACjE;AAAA,MACF;AAAA,IACF;AAGA,eAAW,UAAU,iBAAiB;AACpC,kBAAY,kBAAkB,EAAE,UAAU,CAAC,MAAM,EAAE,CAAC;AAAA,IACtD;AAGA,sBAAkB,oBAAI,IAAI;AAC1B,kBAAc,CAAC;AAAA,EACjB;AAEA,QAAM,cAAc,CAAC,UAAsB;AACzC,oBAAgB,IAAI,MAAM,MAAgB;AAE1C,QAAI,MAAM,IAAI;AACZ,kBAAY,KAAK,EAAE,QAAQ,MAAM,QAAkB,IAAI,MAAM,GAAG,CAAC;AAAA,IACnE;AAGA,QAAI,aAAa,GAAG;AAClB,UAAI,gBAAiB,cAAa,eAAe;AACjD,wBAAkB,WAAW,OAAO,UAAU;AAAA,IAChD,OAAO;AACL,YAAM;AAAA,IACR;AAAA,EACF;AAGA,QAAM,cAAc,UAAU,WAAW;AAGzC,SAAO,MAAM;AACX,QAAI,gBAAiB,cAAa,eAAe;AACjD,gBAAY;AAAA,EACd;AACF;","names":[]}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=index.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.test.d.ts","sourceRoot":"","sources":["../src/index.test.ts"],"names":[],"mappings":""}
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "@rippledb/bind-tanstack",
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "type": "module",
6
+ "sideEffects": false,
7
+ "description": "TanStack Query invalidation binding for RippleDB",
8
+ "keywords": [
9
+ "rippledb",
10
+ "tanstack",
11
+ "tanstack-query",
12
+ "react-query",
13
+ "invalidation",
14
+ "sync",
15
+ "local-first",
16
+ "offline-first"
17
+ ],
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "https://github.com/eckerlein/rippledb.git",
21
+ "directory": "packages/bind-tanstack"
22
+ },
23
+ "exports": {
24
+ ".": {
25
+ "types": "./dist/index.d.ts",
26
+ "import": "./dist/index.js"
27
+ }
28
+ },
29
+ "files": [
30
+ "dist"
31
+ ],
32
+ "dependencies": {
33
+ "@rippledb/core": "0.1.0",
34
+ "@rippledb/client": "0.1.0"
35
+ },
36
+ "peerDependencies": {
37
+ "@tanstack/query-core": "^5.0.0"
38
+ },
39
+ "devDependencies": {
40
+ "@tanstack/query-core": "^5.60.5",
41
+ "eslint": "^9.37.0",
42
+ "tsup": "^8.5.0",
43
+ "typescript": "^5.9.3",
44
+ "vitest": "^3.2.4",
45
+ "@rippledb/store-memory": "0.1.0"
46
+ },
47
+ "tsup": {
48
+ "entry": [
49
+ "src/index.ts"
50
+ ],
51
+ "format": [
52
+ "esm"
53
+ ],
54
+ "dts": false,
55
+ "sourcemap": true,
56
+ "clean": true
57
+ },
58
+ "scripts": {
59
+ "build": "tsup && tsc -p tsconfig.build.json",
60
+ "test": "vitest run --passWithNoTests",
61
+ "test:watch": "vitest --passWithNoTests",
62
+ "lint": "eslint ."
63
+ }
64
+ }