@mongrov/data-access 0.1.0-alpha.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 (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +266 -0
  3. package/dist/context.d.ts +56 -0
  4. package/dist/context.d.ts.map +1 -0
  5. package/dist/context.js +51 -0
  6. package/dist/context.js.map +1 -0
  7. package/dist/define.d.ts +26 -0
  8. package/dist/define.d.ts.map +1 -0
  9. package/dist/define.js +60 -0
  10. package/dist/define.js.map +1 -0
  11. package/dist/dispatcher.d.ts +62 -0
  12. package/dist/dispatcher.d.ts.map +1 -0
  13. package/dist/dispatcher.js +91 -0
  14. package/dist/dispatcher.js.map +1 -0
  15. package/dist/errors.d.ts +27 -0
  16. package/dist/errors.d.ts.map +1 -0
  17. package/dist/errors.js +35 -0
  18. package/dist/errors.js.map +1 -0
  19. package/dist/eslint/index.d.ts +52 -0
  20. package/dist/eslint/index.d.ts.map +1 -0
  21. package/dist/eslint/index.js +45 -0
  22. package/dist/eslint/index.js.map +1 -0
  23. package/dist/eslint/rules/no-storage-engine-imports.d.ts +22 -0
  24. package/dist/eslint/rules/no-storage-engine-imports.d.ts.map +1 -0
  25. package/dist/eslint/rules/no-storage-engine-imports.js +145 -0
  26. package/dist/eslint/rules/no-storage-engine-imports.js.map +1 -0
  27. package/dist/hooks.d.ts +67 -0
  28. package/dist/hooks.d.ts.map +1 -0
  29. package/dist/hooks.js +234 -0
  30. package/dist/hooks.js.map +1 -0
  31. package/dist/index.d.ts +16 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +16 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/invalidation.d.ts +29 -0
  36. package/dist/invalidation.d.ts.map +1 -0
  37. package/dist/invalidation.js +106 -0
  38. package/dist/invalidation.js.map +1 -0
  39. package/dist/tenant.d.ts +18 -0
  40. package/dist/tenant.d.ts.map +1 -0
  41. package/dist/tenant.js +31 -0
  42. package/dist/tenant.js.map +1 -0
  43. package/dist/types.d.ts +127 -0
  44. package/dist/types.d.ts.map +1 -0
  45. package/dist/types.js +11 -0
  46. package/dist/types.js.map +1 -0
  47. package/package.json +91 -0
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Error taxonomy for @mongrov/data-access.
3
+ *
4
+ * Full code set fills in as later phases land. This file ships the class
5
+ * plus the codes stubs need on day one.
6
+ */
7
+ export type DataAccessErrorCode = 'authorization_denied' | 'engine_missing' | 'zod_parse_failed' | 'define_config_invalid' | 'invalid_pattern' | 'not_implemented';
8
+ export declare class DataAccessError extends Error {
9
+ readonly code: DataAccessErrorCode;
10
+ readonly cause?: unknown;
11
+ constructor(code: DataAccessErrorCode, message: string, cause?: unknown);
12
+ }
13
+ /**
14
+ * Thrown when authorization hook rejects a query or mutation.
15
+ * See data-access/spec.md §Authorization.
16
+ */
17
+ export declare class AuthorizationError extends DataAccessError {
18
+ constructor(message: string, cause?: unknown);
19
+ }
20
+ /**
21
+ * Thrown by unimplemented stubs. Distinct class so tests can assert on
22
+ * "this surface isn't wired yet" without ambiguity against runtime errors.
23
+ */
24
+ export declare class NotImplementedError extends DataAccessError {
25
+ constructor(symbol: string);
26
+ }
27
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,MAAM,MAAM,mBAAmB,GAC3B,sBAAsB,GACtB,gBAAgB,GAChB,kBAAkB,GAClB,uBAAuB,GACvB,iBAAiB,GACjB,iBAAiB,CAAA;AAErB,qBAAa,eAAgB,SAAQ,KAAK;IACxC,SAAgB,IAAI,EAAE,mBAAmB,CAAA;IACzC,SAAgB,KAAK,CAAC,EAAE,OAAO,CAAA;gBAEnB,IAAI,EAAE,mBAAmB,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO;CAMxE;AAED;;;GAGG;AACH,qBAAa,kBAAmB,SAAQ,eAAe;gBACzC,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO;CAI7C;AAED;;;GAGG;AACH,qBAAa,mBAAoB,SAAQ,eAAe;gBAC1C,MAAM,EAAE,MAAM;CAO3B"}
package/dist/errors.js ADDED
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Error taxonomy for @mongrov/data-access.
3
+ *
4
+ * Full code set fills in as later phases land. This file ships the class
5
+ * plus the codes stubs need on day one.
6
+ */
7
+ export class DataAccessError extends Error {
8
+ constructor(code, message, cause) {
9
+ super(message);
10
+ this.name = 'DataAccessError';
11
+ this.code = code;
12
+ this.cause = cause;
13
+ }
14
+ }
15
+ /**
16
+ * Thrown when authorization hook rejects a query or mutation.
17
+ * See data-access/spec.md §Authorization.
18
+ */
19
+ export class AuthorizationError extends DataAccessError {
20
+ constructor(message, cause) {
21
+ super('authorization_denied', message, cause);
22
+ this.name = 'AuthorizationError';
23
+ }
24
+ }
25
+ /**
26
+ * Thrown by unimplemented stubs. Distinct class so tests can assert on
27
+ * "this surface isn't wired yet" without ambiguity against runtime errors.
28
+ */
29
+ export class NotImplementedError extends DataAccessError {
30
+ constructor(symbol) {
31
+ super('not_implemented', `${symbol} is not implemented in @mongrov/data-access@0.1.0-alpha.0`);
32
+ this.name = 'NotImplementedError';
33
+ }
34
+ }
35
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAUH,MAAM,OAAO,eAAgB,SAAQ,KAAK;IAIxC,YAAY,IAAyB,EAAE,OAAe,EAAE,KAAe;QACrE,KAAK,CAAC,OAAO,CAAC,CAAA;QACd,IAAI,CAAC,IAAI,GAAG,iBAAiB,CAAA;QAC7B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAA;QAChB,IAAI,CAAC,KAAK,GAAG,KAAK,CAAA;IACpB,CAAC;CACF;AAED;;;GAGG;AACH,MAAM,OAAO,kBAAmB,SAAQ,eAAe;IACrD,YAAY,OAAe,EAAE,KAAe;QAC1C,KAAK,CAAC,sBAAsB,EAAE,OAAO,EAAE,KAAK,CAAC,CAAA;QAC7C,IAAI,CAAC,IAAI,GAAG,oBAAoB,CAAA;IAClC,CAAC;CACF;AAED;;;GAGG;AACH,MAAM,OAAO,mBAAoB,SAAQ,eAAe;IACtD,YAAY,MAAc;QACxB,KAAK,CACH,iBAAiB,EACjB,GAAG,MAAM,2DAA2D,CACrE,CAAA;QACD,IAAI,CAAC,IAAI,GAAG,qBAAqB,CAAA;IACnC,CAAC;CACF"}
@@ -0,0 +1,52 @@
1
+ /**
2
+ * ESLint plugin barrel — `@mongrov/data-access/eslint`.
3
+ *
4
+ * Exposes rule modules under `rules` and a `recommended` flat-config
5
+ * shape under `configs.recommended` that turns the rule on at `'error'`
6
+ * severity. Consumers wire it up like:
7
+ *
8
+ * ```js
9
+ * // eslint.config.mjs
10
+ * import dataAccessPlugin from '@mongrov/data-access/eslint'
11
+ *
12
+ * export default [
13
+ * {
14
+ * plugins: { '@mongrov/data-access': dataAccessPlugin },
15
+ * rules: {
16
+ * '@mongrov/data-access/no-storage-engine-imports': ['error', {
17
+ * allowedDirs: ['src/data'],
18
+ * }],
19
+ * },
20
+ * },
21
+ * ]
22
+ * ```
23
+ *
24
+ * See data-access/spec.md §ESLint plugin.
25
+ */
26
+ declare const rules: {
27
+ readonly 'no-storage-engine-imports': import("eslint").Rule.RuleModule;
28
+ };
29
+ declare const configs: {
30
+ readonly recommended: {
31
+ readonly plugins: readonly ["@mongrov/data-access"];
32
+ readonly rules: {
33
+ readonly "@mongrov/data-access/no-storage-engine-imports": "error";
34
+ };
35
+ };
36
+ };
37
+ declare const plugin: {
38
+ rules: {
39
+ readonly 'no-storage-engine-imports': import("eslint").Rule.RuleModule;
40
+ };
41
+ configs: {
42
+ readonly recommended: {
43
+ readonly plugins: readonly ["@mongrov/data-access"];
44
+ readonly rules: {
45
+ readonly "@mongrov/data-access/no-storage-engine-imports": "error";
46
+ };
47
+ };
48
+ };
49
+ };
50
+ export { rules, configs };
51
+ export default plugin;
52
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/eslint/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAMH,QAAA,MAAM,KAAK;;CAED,CAAA;AAEV,QAAA,MAAM,OAAO;;;;;;;CAOH,CAAA;AAEV,QAAA,MAAM,MAAM;;;;;;;;;;;;CAGX,CAAA;AAED,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,CAAA;AACzB,eAAe,MAAM,CAAA"}
@@ -0,0 +1,45 @@
1
+ /**
2
+ * ESLint plugin barrel — `@mongrov/data-access/eslint`.
3
+ *
4
+ * Exposes rule modules under `rules` and a `recommended` flat-config
5
+ * shape under `configs.recommended` that turns the rule on at `'error'`
6
+ * severity. Consumers wire it up like:
7
+ *
8
+ * ```js
9
+ * // eslint.config.mjs
10
+ * import dataAccessPlugin from '@mongrov/data-access/eslint'
11
+ *
12
+ * export default [
13
+ * {
14
+ * plugins: { '@mongrov/data-access': dataAccessPlugin },
15
+ * rules: {
16
+ * '@mongrov/data-access/no-storage-engine-imports': ['error', {
17
+ * allowedDirs: ['src/data'],
18
+ * }],
19
+ * },
20
+ * },
21
+ * ]
22
+ * ```
23
+ *
24
+ * See data-access/spec.md §ESLint plugin.
25
+ */
26
+ import noStorageEngineImports from './rules/no-storage-engine-imports';
27
+ const PLUGIN_NAME = '@mongrov/data-access';
28
+ const rules = {
29
+ 'no-storage-engine-imports': noStorageEngineImports,
30
+ };
31
+ const configs = {
32
+ recommended: {
33
+ plugins: [PLUGIN_NAME],
34
+ rules: {
35
+ [`${PLUGIN_NAME}/no-storage-engine-imports`]: 'error',
36
+ },
37
+ },
38
+ };
39
+ const plugin = {
40
+ rules,
41
+ configs,
42
+ };
43
+ export { rules, configs };
44
+ export default plugin;
45
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/eslint/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,sBAAsB,MAAM,mCAAmC,CAAA;AAEtE,MAAM,WAAW,GAAG,sBAAsB,CAAA;AAE1C,MAAM,KAAK,GAAG;IACZ,2BAA2B,EAAE,sBAAsB;CAC3C,CAAA;AAEV,MAAM,OAAO,GAAG;IACd,WAAW,EAAE;QACX,OAAO,EAAE,CAAC,WAAW,CAAC;QACtB,KAAK,EAAE;YACL,CAAC,GAAG,WAAW,4BAA4B,CAAC,EAAE,OAAO;SACtD;KACF;CACO,CAAA;AAEV,MAAM,MAAM,GAAG;IACb,KAAK;IACL,OAAO;CACR,CAAA;AAED,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,CAAA;AACzB,eAAe,MAAM,CAAA"}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * ESLint rule — `no-storage-engine-imports`.
3
+ *
4
+ * Prevents screens (and other non-registry files) from reaching directly
5
+ * into the storage-engine packages (@mongrov/analytics, @mongrov/collab,
6
+ * @mongrov/db). All data must flow through named queries + mutations in
7
+ * the app's data-access registry via useAppQuery / useAppMutation.
8
+ *
9
+ * Files under `allowedDirs` are exempt so the registry itself can wire
10
+ * engine adapters.
11
+ *
12
+ * See data-access/spec.md §ESLint plugin.
13
+ */
14
+ import type { Rule } from 'eslint';
15
+ /** Default block list — the three storage engines the spec forbids. */
16
+ declare const DEFAULT_BLOCKED_PACKAGES: string[];
17
+ /** Default allow list — empty; apps supply their registry dirs. */
18
+ declare const DEFAULT_ALLOWED_DIRS: string[];
19
+ declare const rule: Rule.RuleModule;
20
+ export default rule;
21
+ export { DEFAULT_ALLOWED_DIRS, DEFAULT_BLOCKED_PACKAGES };
22
+ //# sourceMappingURL=no-storage-engine-imports.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"no-storage-engine-imports.d.ts","sourceRoot":"","sources":["../../../src/eslint/rules/no-storage-engine-imports.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAA;AAQlC,uEAAuE;AACvE,QAAA,MAAM,wBAAwB,UAI7B,CAAA;AAED,mEAAmE;AACnE,QAAA,MAAM,oBAAoB,EAAE,MAAM,EAAO,CAAA;AAqBzC,QAAA,MAAM,IAAI,EAAE,IAAI,CAAC,UAyEhB,CAAA;AA2CD,eAAe,IAAI,CAAA;AACnB,OAAO,EAAE,oBAAoB,EAAE,wBAAwB,EAAE,CAAA"}
@@ -0,0 +1,145 @@
1
+ /**
2
+ * ESLint rule — `no-storage-engine-imports`.
3
+ *
4
+ * Prevents screens (and other non-registry files) from reaching directly
5
+ * into the storage-engine packages (@mongrov/analytics, @mongrov/collab,
6
+ * @mongrov/db). All data must flow through named queries + mutations in
7
+ * the app's data-access registry via useAppQuery / useAppMutation.
8
+ *
9
+ * Files under `allowedDirs` are exempt so the registry itself can wire
10
+ * engine adapters.
11
+ *
12
+ * See data-access/spec.md §ESLint plugin.
13
+ */
14
+ import * as path from 'node:path';
15
+ /** Default block list — the three storage engines the spec forbids. */
16
+ const DEFAULT_BLOCKED_PACKAGES = [
17
+ '@mongrov/analytics',
18
+ '@mongrov/collab',
19
+ '@mongrov/db',
20
+ ];
21
+ /** Default allow list — empty; apps supply their registry dirs. */
22
+ const DEFAULT_ALLOWED_DIRS = [];
23
+ const optionsSchema = {
24
+ type: 'object',
25
+ properties: {
26
+ allowedDirs: {
27
+ type: 'array',
28
+ items: { type: 'string' },
29
+ description: 'Path segments (relative or absolute) that suppress the rule for files whose path contains any of these fragments. Typical values include the registry directory (e.g. "src/data").',
30
+ },
31
+ blockedPackages: {
32
+ type: 'array',
33
+ items: { type: 'string' },
34
+ description: 'Package names to block. Subpaths are matched (e.g. blocking "@mongrov/db" also blocks "@mongrov/db/kv").',
35
+ },
36
+ },
37
+ additionalProperties: false,
38
+ };
39
+ const rule = {
40
+ meta: {
41
+ type: 'problem',
42
+ docs: {
43
+ description: 'Disallow direct imports of storage-engine packages (@mongrov/analytics, @mongrov/collab, @mongrov/db). Use the data-access registry instead.',
44
+ recommended: true,
45
+ url: 'https://github.com/mongrov/mongrov-packages/tree/main/packages/data-access#eslint-plugin',
46
+ },
47
+ schema: [optionsSchema],
48
+ messages: {
49
+ blockedImport: 'Direct import of "{{importedPackage}}" is blocked in "{{file}}". Route reads through useAppQuery(name, input) and writes through useAppMutation(name) from your data-access registry, or add this directory to `allowedDirs`.',
50
+ },
51
+ },
52
+ create(context) {
53
+ const options = resolveOptions(context.options[0]);
54
+ const filename = context.filename ?? context.getFilename?.() ?? '<input>';
55
+ if (isAllowed(filename, options.allowedDirs)) {
56
+ return {};
57
+ }
58
+ function report(node, importedPackage) {
59
+ context.report({
60
+ node,
61
+ messageId: 'blockedImport',
62
+ data: {
63
+ importedPackage,
64
+ file: relativize(filename),
65
+ },
66
+ });
67
+ }
68
+ function checkSource(node, source) {
69
+ if (!source)
70
+ return;
71
+ const match = matchBlocked(source, options.blockedPackages);
72
+ if (match)
73
+ report(node, match);
74
+ }
75
+ return {
76
+ ImportDeclaration(node) {
77
+ // eslint's AST types leak — cast to any for the source literal.
78
+ const src = node.source
79
+ ?.value;
80
+ if (typeof src === 'string') {
81
+ checkSource(node, src);
82
+ }
83
+ },
84
+ ImportExpression(node) {
85
+ const src = node.source
86
+ ?.value;
87
+ if (typeof src === 'string') {
88
+ checkSource(node, src);
89
+ }
90
+ },
91
+ CallExpression(node) {
92
+ const callee = node
93
+ .callee;
94
+ if (!callee || callee.type !== 'Identifier' || callee.name !== 'require') {
95
+ return;
96
+ }
97
+ const args = node
98
+ .arguments;
99
+ const first = args[0];
100
+ if (first && first.type === 'Literal' && typeof first.value === 'string') {
101
+ checkSource(node, first.value);
102
+ }
103
+ },
104
+ };
105
+ },
106
+ };
107
+ // --- helpers ---------------------------------------------------------
108
+ function resolveOptions(raw) {
109
+ return {
110
+ allowedDirs: raw?.allowedDirs ?? DEFAULT_ALLOWED_DIRS,
111
+ blockedPackages: raw?.blockedPackages ?? DEFAULT_BLOCKED_PACKAGES,
112
+ };
113
+ }
114
+ function isAllowed(filename, allowedDirs) {
115
+ if (allowedDirs.length === 0)
116
+ return false;
117
+ const normalized = filename.split(path.sep).join('/');
118
+ return allowedDirs.some((dir) => {
119
+ const normalizedDir = dir.split(path.sep).join('/');
120
+ return normalized.includes(normalizedDir);
121
+ });
122
+ }
123
+ function matchBlocked(source, blockedPackages) {
124
+ for (const pkg of blockedPackages) {
125
+ if (source === pkg || source.startsWith(`${pkg}/`)) {
126
+ return pkg;
127
+ }
128
+ }
129
+ return null;
130
+ }
131
+ function relativize(filename) {
132
+ if (filename === '<input>')
133
+ return filename;
134
+ try {
135
+ const rel = path.relative(process.cwd(), filename);
136
+ // Show relative form when it's a descendant; keep absolute when it isn't.
137
+ return rel.startsWith('..') ? filename : rel;
138
+ }
139
+ catch {
140
+ return filename;
141
+ }
142
+ }
143
+ export default rule;
144
+ export { DEFAULT_ALLOWED_DIRS, DEFAULT_BLOCKED_PACKAGES };
145
+ //# sourceMappingURL=no-storage-engine-imports.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"no-storage-engine-imports.js","sourceRoot":"","sources":["../../../src/eslint/rules/no-storage-engine-imports.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Screen-facing hooks — useAppQuery / useAppMutation / useAppEvent +
3
+ * useRequestContext accessor.
4
+ *
5
+ * v0.1.0-alpha.0:
6
+ * - useAppQuery (T-11 duckdb, T-12 rxdb, T-13 kv) — TanStack Query
7
+ * bridge for pull engines; observable→state bridge for rxdb.
8
+ * - useAppMutation (T-14) — authorize + optimistic + invalidation
9
+ * with revert-on-failure via cached optimistic snapshot.
10
+ * - useAppEvent (T-15) and useRequestContext (T-17) unchanged.
11
+ *
12
+ * See data-access/spec.md §Hooks.
13
+ */
14
+ import type { RequestContext } from './types';
15
+ /** T-18 — cache defaults per spec §Cache. Per-query overrides win. */
16
+ export declare const DEFAULT_STALE_TIME = 30000;
17
+ export declare const DEFAULT_GC_TIME = 300000;
18
+ export interface UseAppQueryResult<TOutput> {
19
+ data: TOutput | undefined;
20
+ loading: boolean;
21
+ error: Error | null;
22
+ refetch: () => Promise<void>;
23
+ isStale: boolean;
24
+ }
25
+ export interface UseAppMutationResult<TInput, TOutput> {
26
+ mutate: (input: TInput) => void;
27
+ mutateAsync: (input: TInput) => Promise<TOutput>;
28
+ loading: boolean;
29
+ error: Error | null;
30
+ data: TOutput | undefined;
31
+ reset: () => void;
32
+ }
33
+ /**
34
+ * T-11 (duckdb) + T-12 (rxdb) + T-13 (kv) — screen-facing query hook.
35
+ *
36
+ * duckdb/kv → TanStack `useQuery` + invalidation subscription.
37
+ * rxdb → observable subscription; each emission is Zod-parsed and
38
+ * pushed into local state. `refetch` is a no-op (the observable
39
+ * drives updates).
40
+ */
41
+ export declare function useAppQuery<TInput, TOutput>(name: string, input?: TInput): UseAppQueryResult<TOutput>;
42
+ /**
43
+ * T-14 — screen-facing mutation hook.
44
+ *
45
+ * Runs authorize + exec (via the app-provided config) through TanStack
46
+ * useMutation. On success, emits each `invalidates` entry on the event
47
+ * bus so subscribing queries invalidate their caches. If `optimistic`
48
+ * is defined, the returned `data` reflects the optimistic value while
49
+ * the mutation is pending and reverts on failure.
50
+ */
51
+ export declare function useAppMutation<TInput, TOutput>(name: string): UseAppMutationResult<TInput, TOutput>;
52
+ /**
53
+ * T-15 — subscribe to a named event for the lifetime of the mounting
54
+ * component. Handler runs on every emit whose name matches exactly; use
55
+ * the pattern form (bus.subscribePattern) directly for glob semantics.
56
+ *
57
+ * The handler ref is kept fresh so callers may pass unstable closures
58
+ * without churning the subscription.
59
+ */
60
+ export declare function useAppEvent<TPayload>(name: string, handler: (payload: TPayload) => void): void;
61
+ /**
62
+ * T-17 — read the current session's RequestContext from the provider.
63
+ * The provider's `context` callback is invoked on every read so
64
+ * callers pick up session updates without re-mounting.
65
+ */
66
+ export declare function useRequestContext(): RequestContext;
67
+ //# sourceMappingURL=hooks.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../src/hooks.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AASH,OAAO,KAAK,EAGV,cAAc,EAEf,MAAM,SAAS,CAAA;AAEhB,sEAAsE;AACtE,eAAO,MAAM,kBAAkB,QAAS,CAAA;AACxC,eAAO,MAAM,eAAe,SAAU,CAAA;AAEtC,MAAM,WAAW,iBAAiB,CAAC,OAAO;IACxC,IAAI,EAAE,OAAO,GAAG,SAAS,CAAA;IACzB,OAAO,EAAE,OAAO,CAAA;IAChB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAA;IACnB,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;IAC5B,OAAO,EAAE,OAAO,CAAA;CACjB;AAED,MAAM,WAAW,oBAAoB,CAAC,MAAM,EAAE,OAAO;IACnD,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IAC/B,WAAW,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;IAChD,OAAO,EAAE,OAAO,CAAA;IAChB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAA;IACnB,IAAI,EAAE,OAAO,GAAG,SAAS,CAAA;IACzB,KAAK,EAAE,MAAM,IAAI,CAAA;CAClB;AAcD;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,OAAO,EACzC,IAAI,EAAE,MAAM,EACZ,KAAK,CAAC,EAAE,MAAM,GACb,iBAAiB,CAAC,OAAO,CAAC,CA+H5B;AAED;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,OAAO,EAC5C,IAAI,EAAE,MAAM,GACX,oBAAoB,CAAC,MAAM,EAAE,OAAO,CAAC,CA2DvC;AAED;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAClC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,CAAC,OAAO,EAAE,QAAQ,KAAK,IAAI,GACnC,IAAI,CAcN;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,IAAI,cAAc,CAGlD"}
package/dist/hooks.js ADDED
@@ -0,0 +1,234 @@
1
+ /**
2
+ * Screen-facing hooks — useAppQuery / useAppMutation / useAppEvent +
3
+ * useRequestContext accessor.
4
+ *
5
+ * v0.1.0-alpha.0:
6
+ * - useAppQuery (T-11 duckdb, T-12 rxdb, T-13 kv) — TanStack Query
7
+ * bridge for pull engines; observable→state bridge for rxdb.
8
+ * - useAppMutation (T-14) — authorize + optimistic + invalidation
9
+ * with revert-on-failure via cached optimistic snapshot.
10
+ * - useAppEvent (T-15) and useRequestContext (T-17) unchanged.
11
+ *
12
+ * See data-access/spec.md §Hooks.
13
+ */
14
+ import { useMutation, useQuery } from '@tanstack/react-query';
15
+ import * as React from 'react';
16
+ import { useDataAccessRuntime } from './context';
17
+ import { executeQuery, runAuthorize } from './dispatcher';
18
+ import { DataAccessError } from './errors';
19
+ /** T-18 — cache defaults per spec §Cache. Per-query overrides win. */
20
+ export const DEFAULT_STALE_TIME = 30000;
21
+ export const DEFAULT_GC_TIME = 300000;
22
+ /**
23
+ * T-11 (duckdb) + T-12 (rxdb) + T-13 (kv) — screen-facing query hook.
24
+ *
25
+ * duckdb/kv → TanStack `useQuery` + invalidation subscription.
26
+ * rxdb → observable subscription; each emission is Zod-parsed and
27
+ * pushed into local state. `refetch` is a no-op (the observable
28
+ * drives updates).
29
+ */
30
+ export function useAppQuery(name, input) {
31
+ const runtime = useDataAccessRuntime();
32
+ const def = lookupDefinition(runtime.registry.queries, 'query', name);
33
+ const engine = def.config.engine;
34
+ const isRxdb = engine === 'rxdb';
35
+ const queryKey = React.useMemo(() => [name, input], [name, input]);
36
+ // TanStack path — active for duckdb + kv, disabled for rxdb.
37
+ const query = useQuery({
38
+ queryKey,
39
+ queryFn: () => executeQuery(def, input, runtime.getContext(), runtime.engines),
40
+ enabled: !isRxdb,
41
+ staleTime: def.config.staleTime ?? DEFAULT_STALE_TIME,
42
+ gcTime: def.config.gcTime ?? DEFAULT_GC_TIME,
43
+ });
44
+ // rxdb path — external state driven by observable emissions.
45
+ const [rxState, setRxState] = React.useState({ data: undefined, loading: true, error: null });
46
+ React.useEffect(() => {
47
+ if (!isRxdb)
48
+ return;
49
+ const rxEngine = runtime.engines.rxdb;
50
+ if (!rxEngine) {
51
+ setRxState({
52
+ data: undefined,
53
+ loading: false,
54
+ error: new DataAccessError('engine_missing', "engine 'rxdb' is not wired in this dispatcher"),
55
+ });
56
+ return;
57
+ }
58
+ const observable = def.config.query(rxEngine.db, input);
59
+ let cancelled = false;
60
+ const sub = observable.subscribe({
61
+ next: (value) => {
62
+ if (cancelled)
63
+ return;
64
+ const parsed = def.config.output.safeParse(value);
65
+ if (parsed.success) {
66
+ setRxState({
67
+ data: parsed.data,
68
+ loading: false,
69
+ error: null,
70
+ });
71
+ }
72
+ else {
73
+ setRxState({
74
+ data: undefined,
75
+ loading: false,
76
+ error: new DataAccessError('zod_parse_failed', `query "${name}" output failed schema validation: ${parsed.error.message}`, parsed.error),
77
+ });
78
+ }
79
+ },
80
+ error: (err) => {
81
+ if (cancelled)
82
+ return;
83
+ setRxState({
84
+ data: undefined,
85
+ loading: false,
86
+ error: err instanceof Error ? err : new Error(String(err)),
87
+ });
88
+ },
89
+ });
90
+ return () => {
91
+ cancelled = true;
92
+ sub.unsubscribe();
93
+ };
94
+ }, [runtime, def, input, isRxdb, name]);
95
+ // Invalidation subscriptions — TanStack path only. rxdb self-updates
96
+ // via the observable stream.
97
+ const patterns = def.config.invalidatedBy;
98
+ React.useEffect(() => {
99
+ if (isRxdb)
100
+ return;
101
+ if (!patterns || patterns.length === 0)
102
+ return;
103
+ const unsubs = patterns.map((pattern) => runtime.bus.subscribePattern(pattern, () => {
104
+ runtime.queryClient.invalidateQueries({ queryKey: [name] });
105
+ }));
106
+ return () => {
107
+ for (const off of unsubs)
108
+ off();
109
+ };
110
+ }, [runtime.bus, runtime.queryClient, name, patterns, isRxdb]);
111
+ const refetch = React.useCallback(async () => {
112
+ if (isRxdb)
113
+ return;
114
+ await query.refetch();
115
+ }, [isRxdb, query]);
116
+ if (isRxdb) {
117
+ return {
118
+ data: rxState.data,
119
+ loading: rxState.loading,
120
+ error: rxState.error,
121
+ refetch,
122
+ isStale: false,
123
+ };
124
+ }
125
+ return {
126
+ data: query.data,
127
+ loading: query.isPending,
128
+ error: query.error ?? null,
129
+ refetch,
130
+ isStale: query.isStale,
131
+ };
132
+ }
133
+ /**
134
+ * T-14 — screen-facing mutation hook.
135
+ *
136
+ * Runs authorize + exec (via the app-provided config) through TanStack
137
+ * useMutation. On success, emits each `invalidates` entry on the event
138
+ * bus so subscribing queries invalidate their caches. If `optimistic`
139
+ * is defined, the returned `data` reflects the optimistic value while
140
+ * the mutation is pending and reverts on failure.
141
+ */
142
+ export function useAppMutation(name) {
143
+ const runtime = useDataAccessRuntime();
144
+ const def = lookupDefinition(runtime.registry.mutations, 'mutation', name);
145
+ const [optimisticValue, setOptimisticValue] = React.useState(undefined);
146
+ const mutation = useMutation({
147
+ mutationFn: async (input) => {
148
+ const ctx = runtime.getContext();
149
+ const parsedInput = parseWithSchema(def.config.input, input, 'input', name);
150
+ await runAuthorize(def.config.authorize, parsedInput, ctx);
151
+ const raw = await def.config.exec(parsedInput, ctx);
152
+ return parseWithSchema(def.config.output, raw, 'output', name);
153
+ },
154
+ onMutate: (input) => {
155
+ if (!def.config.optimistic)
156
+ return undefined;
157
+ const ctx = runtime.getContext();
158
+ const value = def.config.optimistic(input, ctx);
159
+ setOptimisticValue(value);
160
+ return undefined;
161
+ },
162
+ onSuccess: () => {
163
+ const patterns = def.config.invalidates ?? [];
164
+ for (const pattern of patterns) {
165
+ runtime.bus.emit(pattern, undefined);
166
+ }
167
+ },
168
+ onSettled: () => {
169
+ // Clear the optimistic snapshot so subsequent mutations start
170
+ // clean. On failure this achieves the "revert" — mutation.data
171
+ // returns to undefined and the optimistic layer disappears.
172
+ setOptimisticValue(undefined);
173
+ },
174
+ });
175
+ const data = mutation.isPending && optimisticValue !== undefined
176
+ ? optimisticValue
177
+ : mutation.data;
178
+ return {
179
+ mutate: mutation.mutate,
180
+ mutateAsync: mutation.mutateAsync,
181
+ loading: mutation.isPending,
182
+ error: mutation.error ?? null,
183
+ data,
184
+ reset: mutation.reset,
185
+ };
186
+ }
187
+ /**
188
+ * T-15 — subscribe to a named event for the lifetime of the mounting
189
+ * component. Handler runs on every emit whose name matches exactly; use
190
+ * the pattern form (bus.subscribePattern) directly for glob semantics.
191
+ *
192
+ * The handler ref is kept fresh so callers may pass unstable closures
193
+ * without churning the subscription.
194
+ */
195
+ export function useAppEvent(name, handler) {
196
+ const runtime = useDataAccessRuntime();
197
+ const handlerRef = React.useRef(handler);
198
+ React.useEffect(() => {
199
+ handlerRef.current = handler;
200
+ }, [handler]);
201
+ React.useEffect(() => {
202
+ const off = runtime.bus.subscribe(name, (payload) => {
203
+ handlerRef.current(payload);
204
+ });
205
+ return off;
206
+ }, [runtime.bus, name]);
207
+ }
208
+ /**
209
+ * T-17 — read the current session's RequestContext from the provider.
210
+ * The provider's `context` callback is invoked on every read so
211
+ * callers pick up session updates without re-mounting.
212
+ */
213
+ export function useRequestContext() {
214
+ const runtime = useDataAccessRuntime();
215
+ return runtime.getContext();
216
+ }
217
+ // --- helpers ---------------------------------------------------------
218
+ function lookupDefinition(bag, label, name) {
219
+ const def = bag[name];
220
+ if (!def) {
221
+ throw new DataAccessError('engine_missing', `no ${label} named "${name}" is registered`);
222
+ }
223
+ return def;
224
+ }
225
+ function parseWithSchema(schema, value, side, name) {
226
+ if (!schema)
227
+ return value;
228
+ const result = schema.safeParse(value);
229
+ if (!result.success) {
230
+ throw new DataAccessError('zod_parse_failed', `mutation "${name}" ${side} failed schema validation: ${result.error.message}`, result.error);
231
+ }
232
+ return result.data;
233
+ }
234
+ //# sourceMappingURL=hooks.js.map