zodbridge 0.2.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,140 @@
1
+ import { z } from 'zod';
2
+
3
+ /**
4
+ * Resolver-graph context: dependency-aware, memoizing, cycle-guarded lazy field
5
+ * resolution. A faithful generalization of a `ContextResolver`-style engine.
6
+ *
7
+ * Invariants (do not "optimize" away):
8
+ * (a) The promise for a field is stored in `cache` BEFORE the first await, so
9
+ * concurrent `Promise.all` over a shared dep fires its resolver once.
10
+ * (b) The touched-check (`cache` ∪ `inProgress`) serializes a field's re-entry,
11
+ * breaking cycles.
12
+ * (c) A resolver that THROWS rejects and evicts its cache entry — errors are
13
+ * never coerced to `undefined` and never cached. Only a RETURNED `undefined`
14
+ * is cached as a terminal value.
15
+ * (d) `fallback` retries the calling field by re-invoking its resolver FUNCTION
16
+ * directly, bypassing the step-1 cache short-circuit, so a momentarily
17
+ * `undefined` field can still resolve once a dependency becomes available.
18
+ * (e) The calling field for `fallback` is bound per resolver invocation (a
19
+ * field-bound context), NOT read from shared instance state — so two
20
+ * fallback-using resolvers running concurrently never contaminate each
21
+ * other's retry target.
22
+ */
23
+
24
+ /** A resolver function for one field; may use the context to derive its value. */
25
+ type ResolverFn<Fields, TAdapter, K extends keyof Fields> = (ctx: ResolverContext<Fields, TAdapter>) => Fields[K] | undefined | Promise<Fields[K] | undefined>;
26
+ /** Registry of per-field resolvers (declarative; not class methods). */
27
+ type ResolverRegistry<Fields, TAdapter> = {
28
+ [K in keyof Fields]?: ResolverFn<Fields, TAdapter, K>;
29
+ };
30
+ /**
31
+ * Per-field Zod schemas used to validate resolver outputs at runtime. Built by
32
+ * the schema-driven `createResolver` overload from `maps` + `fields`; a field
33
+ * with no entry here is not validated (e.g. hand-written-`Fields` callers).
34
+ */
35
+ type ResolverSchemas<Fields> = Partial<Record<keyof Fields, z.ZodType>>;
36
+ /** Construction config for a resolver graph. */
37
+ interface ResolverConfig<Fields, TAdapter> {
38
+ adapter: TAdapter;
39
+ seed?: Partial<Fields>;
40
+ resolvers?: ResolverRegistry<Fields, TAdapter>;
41
+ /**
42
+ * Optional per-field schemas. When present for a field, its resolver's output
43
+ * is parsed (and unknown keys stripped) before caching; a parse failure
44
+ * rejects `resolve` and evicts the entry. Seed values are NOT validated.
45
+ */
46
+ schemas?: ResolverSchemas<Fields>;
47
+ /**
48
+ * Optional observer fired after a field's resolver settles successfully (post
49
+ * validation), for tracing/debugging. Never fired for seed/terminal values.
50
+ * Throwing here does not affect resolution (the hook is best-effort).
51
+ */
52
+ onResolve?: (field: keyof Fields, value: unknown) => void;
53
+ }
54
+ /** The context object handed to every resolver and returned to callers. */
55
+ interface ResolverContext<Fields, TAdapter> {
56
+ readonly adapter: TAdapter;
57
+ /** Resolve one field: cache -> seed -> resolver -> `undefined`. Memoized. */
58
+ resolve<K extends keyof Fields>(field: K): Promise<Fields[K] | undefined>;
59
+ /** Resolve several fields; returns only the requested keys. */
60
+ resolveMany<K extends keyof Fields>(...fields: K[]): Promise<Partial<Pick<Fields, K>>>;
61
+ /** Cache peek: the resolved value if present, else `undefined`. No I/O. */
62
+ get<K extends keyof Fields>(field: K): Fields[K] | undefined;
63
+ /** Pure nested peek into seed + cache for `field`. Never resolves, never I/O. */
64
+ path<K extends keyof Fields>(field: K, keyPath: string[]): unknown;
65
+ /** Resolve untouched `deps`, then retry the calling field's resolver fn. */
66
+ fallback(deps: Array<keyof Fields>): Promise<unknown>;
67
+ /**
68
+ * Forget a field's cached value (and any settled peek) so the next `resolve`
69
+ * re-runs its resolver. Use after a write to force a fresh fetch from the
70
+ * adapter. Seed values are also dropped for the field. Returns `this`.
71
+ */
72
+ invalidate(...fields: Array<keyof Fields>): ResolverContext<Fields, TAdapter>;
73
+ /** Invalidate then re-resolve `field` — a forced sync re-fetch from the adapter. */
74
+ refresh<K extends keyof Fields>(field: K): Promise<Fields[K] | undefined>;
75
+ }
76
+ declare class GraphContext<Fields, TAdapter> implements ResolverContext<Fields, TAdapter> {
77
+ readonly adapter: TAdapter;
78
+ private readonly seed;
79
+ private readonly resolvers;
80
+ private readonly schemas;
81
+ private readonly onResolve?;
82
+ /** Durable promise-cache: every attempted field lives here. */
83
+ private readonly cache;
84
+ /** Synchronously-readable settled values, for `get`/`path` peeks. */
85
+ private readonly settled;
86
+ /** Fields with an in-flight resolver; transient cycle guard. */
87
+ private readonly inProgress;
88
+ /** Fields explicitly invalidated: prefer the resolver over seed on next resolve. */
89
+ private readonly invalidated;
90
+ constructor(config: ResolverConfig<Fields, TAdapter>);
91
+ /** Synchronous peek of seed first, then any settled resolved value. */
92
+ private peek;
93
+ get<K extends keyof Fields>(field: K): Fields[K] | undefined;
94
+ path<K extends keyof Fields>(field: K, keyPath: string[]): unknown;
95
+ /** Public entry: a fresh resolution chain with no ancestors. */
96
+ resolve<K extends keyof Fields>(field: K): Promise<Fields[K] | undefined>;
97
+ /**
98
+ * Resolve `field` within a chain whose currently-resolving fields are
99
+ * `ancestors`. If `field` is itself an ancestor, this is a dependency cycle
100
+ * (mutual A<->B, longer A->B->C->A, or self x->x): return `undefined` so the
101
+ * caller (e.g. a `strategies` candidate) falls through to its next route,
102
+ * instead of awaiting its own still-pending promise (which would deadlock).
103
+ *
104
+ * The ancestor check runs BEFORE the `cache.has` short-circuit, so a CONCURRENT
105
+ * caller (a parallel `Promise.all` chain — `field` is in-flight but NOT its
106
+ * ancestor) still receives the shared pending promise and dedups normally.
107
+ */
108
+ private resolveWithin;
109
+ /**
110
+ * A context bound to `self` and the chain's `ancestors`: identical to the
111
+ * top-level context except `resolve`/`fallback` carry the chain so cycles
112
+ * break (see {@link resolveWithin}) and `fallback` retries the right field.
113
+ * Binding per invocation (not shared instance state) keeps concurrent
114
+ * resolutions from contaminating each other.
115
+ */
116
+ private boundContext;
117
+ invalidate(...fields: Array<keyof Fields>): ResolverContext<Fields, TAdapter>;
118
+ refresh<K extends keyof Fields>(field: K): Promise<Fields[K] | undefined>;
119
+ /** Fire the best-effort `onResolve` observer; never let it break resolution. */
120
+ private emitResolved;
121
+ private runResolver;
122
+ /**
123
+ * Validate a resolver's output against its field schema (if any). A defined
124
+ * value is parsed (unknown keys stripped); a parse failure throws the ZodError,
125
+ * which the reject branch turns into an eviction. `undefined` is the forgiving
126
+ * absence terminal and is never validated.
127
+ */
128
+ private validate;
129
+ /** Resolve untouched `deps`, then retry `self`'s resolver if any produced a value. */
130
+ private fallbackFor;
131
+ /**
132
+ * Top-level `fallback` has no calling field, so there is nothing to retry.
133
+ * Resolvers always receive a field-bound context whose `fallback` does retry;
134
+ * this entry exists only to satisfy the public {@link ResolverContext} shape.
135
+ */
136
+ fallback(_deps: Array<keyof Fields>): Promise<unknown>;
137
+ resolveMany<K extends keyof Fields>(...fields: K[]): Promise<Partial<Pick<Fields, K>>>;
138
+ }
139
+
140
+ export { GraphContext as G, type ResolverContext as R, type ResolverRegistry as a, type ResolverConfig as b, type ResolverFn as c, type ResolverSchemas as d };
@@ -0,0 +1,140 @@
1
+ import { z } from 'zod';
2
+
3
+ /**
4
+ * Resolver-graph context: dependency-aware, memoizing, cycle-guarded lazy field
5
+ * resolution. A faithful generalization of a `ContextResolver`-style engine.
6
+ *
7
+ * Invariants (do not "optimize" away):
8
+ * (a) The promise for a field is stored in `cache` BEFORE the first await, so
9
+ * concurrent `Promise.all` over a shared dep fires its resolver once.
10
+ * (b) The touched-check (`cache` ∪ `inProgress`) serializes a field's re-entry,
11
+ * breaking cycles.
12
+ * (c) A resolver that THROWS rejects and evicts its cache entry — errors are
13
+ * never coerced to `undefined` and never cached. Only a RETURNED `undefined`
14
+ * is cached as a terminal value.
15
+ * (d) `fallback` retries the calling field by re-invoking its resolver FUNCTION
16
+ * directly, bypassing the step-1 cache short-circuit, so a momentarily
17
+ * `undefined` field can still resolve once a dependency becomes available.
18
+ * (e) The calling field for `fallback` is bound per resolver invocation (a
19
+ * field-bound context), NOT read from shared instance state — so two
20
+ * fallback-using resolvers running concurrently never contaminate each
21
+ * other's retry target.
22
+ */
23
+
24
+ /** A resolver function for one field; may use the context to derive its value. */
25
+ type ResolverFn<Fields, TAdapter, K extends keyof Fields> = (ctx: ResolverContext<Fields, TAdapter>) => Fields[K] | undefined | Promise<Fields[K] | undefined>;
26
+ /** Registry of per-field resolvers (declarative; not class methods). */
27
+ type ResolverRegistry<Fields, TAdapter> = {
28
+ [K in keyof Fields]?: ResolverFn<Fields, TAdapter, K>;
29
+ };
30
+ /**
31
+ * Per-field Zod schemas used to validate resolver outputs at runtime. Built by
32
+ * the schema-driven `createResolver` overload from `maps` + `fields`; a field
33
+ * with no entry here is not validated (e.g. hand-written-`Fields` callers).
34
+ */
35
+ type ResolverSchemas<Fields> = Partial<Record<keyof Fields, z.ZodType>>;
36
+ /** Construction config for a resolver graph. */
37
+ interface ResolverConfig<Fields, TAdapter> {
38
+ adapter: TAdapter;
39
+ seed?: Partial<Fields>;
40
+ resolvers?: ResolverRegistry<Fields, TAdapter>;
41
+ /**
42
+ * Optional per-field schemas. When present for a field, its resolver's output
43
+ * is parsed (and unknown keys stripped) before caching; a parse failure
44
+ * rejects `resolve` and evicts the entry. Seed values are NOT validated.
45
+ */
46
+ schemas?: ResolverSchemas<Fields>;
47
+ /**
48
+ * Optional observer fired after a field's resolver settles successfully (post
49
+ * validation), for tracing/debugging. Never fired for seed/terminal values.
50
+ * Throwing here does not affect resolution (the hook is best-effort).
51
+ */
52
+ onResolve?: (field: keyof Fields, value: unknown) => void;
53
+ }
54
+ /** The context object handed to every resolver and returned to callers. */
55
+ interface ResolverContext<Fields, TAdapter> {
56
+ readonly adapter: TAdapter;
57
+ /** Resolve one field: cache -> seed -> resolver -> `undefined`. Memoized. */
58
+ resolve<K extends keyof Fields>(field: K): Promise<Fields[K] | undefined>;
59
+ /** Resolve several fields; returns only the requested keys. */
60
+ resolveMany<K extends keyof Fields>(...fields: K[]): Promise<Partial<Pick<Fields, K>>>;
61
+ /** Cache peek: the resolved value if present, else `undefined`. No I/O. */
62
+ get<K extends keyof Fields>(field: K): Fields[K] | undefined;
63
+ /** Pure nested peek into seed + cache for `field`. Never resolves, never I/O. */
64
+ path<K extends keyof Fields>(field: K, keyPath: string[]): unknown;
65
+ /** Resolve untouched `deps`, then retry the calling field's resolver fn. */
66
+ fallback(deps: Array<keyof Fields>): Promise<unknown>;
67
+ /**
68
+ * Forget a field's cached value (and any settled peek) so the next `resolve`
69
+ * re-runs its resolver. Use after a write to force a fresh fetch from the
70
+ * adapter. Seed values are also dropped for the field. Returns `this`.
71
+ */
72
+ invalidate(...fields: Array<keyof Fields>): ResolverContext<Fields, TAdapter>;
73
+ /** Invalidate then re-resolve `field` — a forced sync re-fetch from the adapter. */
74
+ refresh<K extends keyof Fields>(field: K): Promise<Fields[K] | undefined>;
75
+ }
76
+ declare class GraphContext<Fields, TAdapter> implements ResolverContext<Fields, TAdapter> {
77
+ readonly adapter: TAdapter;
78
+ private readonly seed;
79
+ private readonly resolvers;
80
+ private readonly schemas;
81
+ private readonly onResolve?;
82
+ /** Durable promise-cache: every attempted field lives here. */
83
+ private readonly cache;
84
+ /** Synchronously-readable settled values, for `get`/`path` peeks. */
85
+ private readonly settled;
86
+ /** Fields with an in-flight resolver; transient cycle guard. */
87
+ private readonly inProgress;
88
+ /** Fields explicitly invalidated: prefer the resolver over seed on next resolve. */
89
+ private readonly invalidated;
90
+ constructor(config: ResolverConfig<Fields, TAdapter>);
91
+ /** Synchronous peek of seed first, then any settled resolved value. */
92
+ private peek;
93
+ get<K extends keyof Fields>(field: K): Fields[K] | undefined;
94
+ path<K extends keyof Fields>(field: K, keyPath: string[]): unknown;
95
+ /** Public entry: a fresh resolution chain with no ancestors. */
96
+ resolve<K extends keyof Fields>(field: K): Promise<Fields[K] | undefined>;
97
+ /**
98
+ * Resolve `field` within a chain whose currently-resolving fields are
99
+ * `ancestors`. If `field` is itself an ancestor, this is a dependency cycle
100
+ * (mutual A<->B, longer A->B->C->A, or self x->x): return `undefined` so the
101
+ * caller (e.g. a `strategies` candidate) falls through to its next route,
102
+ * instead of awaiting its own still-pending promise (which would deadlock).
103
+ *
104
+ * The ancestor check runs BEFORE the `cache.has` short-circuit, so a CONCURRENT
105
+ * caller (a parallel `Promise.all` chain — `field` is in-flight but NOT its
106
+ * ancestor) still receives the shared pending promise and dedups normally.
107
+ */
108
+ private resolveWithin;
109
+ /**
110
+ * A context bound to `self` and the chain's `ancestors`: identical to the
111
+ * top-level context except `resolve`/`fallback` carry the chain so cycles
112
+ * break (see {@link resolveWithin}) and `fallback` retries the right field.
113
+ * Binding per invocation (not shared instance state) keeps concurrent
114
+ * resolutions from contaminating each other.
115
+ */
116
+ private boundContext;
117
+ invalidate(...fields: Array<keyof Fields>): ResolverContext<Fields, TAdapter>;
118
+ refresh<K extends keyof Fields>(field: K): Promise<Fields[K] | undefined>;
119
+ /** Fire the best-effort `onResolve` observer; never let it break resolution. */
120
+ private emitResolved;
121
+ private runResolver;
122
+ /**
123
+ * Validate a resolver's output against its field schema (if any). A defined
124
+ * value is parsed (unknown keys stripped); a parse failure throws the ZodError,
125
+ * which the reject branch turns into an eviction. `undefined` is the forgiving
126
+ * absence terminal and is never validated.
127
+ */
128
+ private validate;
129
+ /** Resolve untouched `deps`, then retry `self`'s resolver if any produced a value. */
130
+ private fallbackFor;
131
+ /**
132
+ * Top-level `fallback` has no calling field, so there is nothing to retry.
133
+ * Resolvers always receive a field-bound context whose `fallback` does retry;
134
+ * this entry exists only to satisfy the public {@link ResolverContext} shape.
135
+ */
136
+ fallback(_deps: Array<keyof Fields>): Promise<unknown>;
137
+ resolveMany<K extends keyof Fields>(...fields: K[]): Promise<Partial<Pick<Fields, K>>>;
138
+ }
139
+
140
+ export { GraphContext as G, type ResolverContext as R, type ResolverRegistry as a, type ResolverConfig as b, type ResolverFn as c, type ResolverSchemas as d };