@ignex/ninox 0.1.1

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,95 @@
1
+ /**
2
+ * Logger abstraction. The ORM never logs directly — it accepts any logger that
3
+ * satisfies `LoggerLike` (pino, a console wrapper, a test stub, ...). This keeps
4
+ * the package free of logging dependencies while remaining observability-agnostic.
5
+ */
6
+ interface LogFn {
7
+ (obj: Record<string, unknown>, msg?: string): void;
8
+ (msg: string): void;
9
+ }
10
+ interface LoggerLike {
11
+ debug: LogFn;
12
+ info: LogFn;
13
+ warn: LogFn;
14
+ error: LogFn;
15
+ }
16
+ /** A logger that swallows everything. Useful in tests / perf runs. */
17
+ declare const createNoopLogger: () => LoggerLike;
18
+ declare const LEVELS: readonly ["debug", "info", "warn", "error"];
19
+ type LogLevel = (typeof LEVELS)[number];
20
+ /**
21
+ * A tiny console logger matching the `LoggerLike` shape. `level` filters output.
22
+ */
23
+ declare const createConsoleLogger: (level?: LogLevel) => LoggerLike;
24
+
25
+ /**
26
+ * Minimal dependency-free LRU cache backed by a Map (recency = insertion order).
27
+ * get() re-inserts the entry so the most-recently-used item is always last.
28
+ */
29
+ interface LRUOptions {
30
+ /** Maximum number of entries before the least-recently-used item is evicted. Must be > 0. */
31
+ max: number;
32
+ }
33
+ declare class LRU<K, V> {
34
+ readonly max: number;
35
+ private map;
36
+ /** Number of entries evicted to make room for new ones (capacity evictions only). */
37
+ evictions: number;
38
+ constructor(options: LRUOptions);
39
+ get size(): number;
40
+ get(key: K): V | undefined;
41
+ peek(key: K): V | undefined;
42
+ has(key: K): boolean;
43
+ set(key: K, value: V): this;
44
+ delete(key: K): boolean;
45
+ clear(): void;
46
+ keys(): IterableIterator<K>;
47
+ entries(): IterableIterator<[K, V]>;
48
+ [Symbol.iterator](): IterableIterator<[K, V]>;
49
+ }
50
+
51
+ /**
52
+ * Deterministic hashing utilities used for cache keys / in-flight dedup keys.
53
+ * `stableStringify` produces an order-insensitive serialization so two logically
54
+ * identical filters yield the same hash regardless of key insertion order.
55
+ */
56
+ /**
57
+ * Stable stringify: recursively sorts object keys, coerces Date/ObjectId/RegExp
58
+ * into primitive markers. Result is deterministic for a given logical value.
59
+ */
60
+ declare const stableStringify: (value: unknown) => string;
61
+ /** djb2 string hash → base-36 (fits comfortably in a double, no BigInt). */
62
+ declare const hashString: (input: string) => string;
63
+ /** Convenience: hash any value through a stable stringification. */
64
+ declare const stableHash: (value: unknown) => string;
65
+
66
+ interface CacheOptions {
67
+ /** Max cached entries. `0` or negative → unbounded Map (default 100). */
68
+ maxSize?: number;
69
+ }
70
+ /**
71
+ * Synchronous keyed factory cache: one value per key, LRU-bounded.
72
+ * Used for cheap singleton factories (connection pools, resolvers, ...).
73
+ */
74
+ declare const createCachedFactory: <K, V>(factory: (key: K) => V, options?: CacheOptions) => ((key: K) => V);
75
+ /**
76
+ * Async keyed factory cache with **in-flight deduplication**: concurrent calls
77
+ * with the same key share a single Promise so the factory runs only once.
78
+ * On settle (success or failure) the in-flight entry is removed.
79
+ */
80
+ declare const createCachedAsyncFactory: <K, V>(factory: (key: K) => Promise<V>, options?: CacheOptions) => ((key: K) => Promise<V>);
81
+
82
+ /**
83
+ * Race a promise against a timeout. A `timeoutMs` <= 0 disables the timer and
84
+ * returns the original promise untouched (zero-overhead fast path).
85
+ */
86
+ declare const withTimeout: <T>(promise: Promise<T>, timeoutMs: number, label: string) => Promise<T>;
87
+ /** Sleep helper used by retry backoff. */
88
+ declare const sleep: (ms: number) => Promise<void>;
89
+ /**
90
+ * Sleep for `ms` plus random jitter (0..jitterMs, capped at `ms`) so retry
91
+ * loops don't resynchronize into a thundering herd after a replica failover.
92
+ */
93
+ declare const sleepJittered: (ms: number, jitterMs?: number) => Promise<void>;
94
+
95
+ export { type CacheOptions as C, type LoggerLike as L, LRU as a, type LRUOptions as b, type LogFn as c, type LogLevel as d, createCachedAsyncFactory as e, createCachedFactory as f, createConsoleLogger as g, createNoopLogger as h, hashString as i, sleepJittered as j, stableHash as k, stableStringify as l, sleep as s, withTimeout as w };
@@ -0,0 +1,13 @@
1
+ export { C as CacheOptions, a as LRU, b as LRUOptions, c as LogFn, d as LogLevel, L as LoggerLike, e as createCachedAsyncFactory, f as createCachedFactory, g as createConsoleLogger, h as createNoopLogger, i as hashString, s as sleep, j as sleepJittered, k as stableHash, l as stableStringify, w as withTimeout } from './timeout-BgrqtMnd.js';
2
+
3
+ /**
4
+ * Deep-clone a value while preserving the BSON-ish types that
5
+ * `JSON.parse(JSON.stringify())` would corrupt: `Date`, `ObjectId`, and
6
+ * `RegExp`. Arrays and plain objects are copied recursively; primitives pass
7
+ * through by reference. Used by the optional `clone` mode on `QueryCache` /
8
+ * `HotCache` so a caller can't mutate a cached result and poison the shared
9
+ * cache.
10
+ */
11
+ declare const cloneDeep: <T>(value: T) => T;
12
+
13
+ export { cloneDeep };
package/dist/utils.js ADDED
@@ -0,0 +1,223 @@
1
+ import { ObjectId } from 'mongodb';
2
+
3
+ // src/utils/clone.ts
4
+ var cloneDeep = (value) => {
5
+ if (value instanceof Date) return new Date(value.getTime());
6
+ if (value instanceof ObjectId) return new ObjectId(value.toHexString());
7
+ if (value instanceof RegExp) return new RegExp(value.source, value.flags);
8
+ if (Array.isArray(value)) return value.map((item) => cloneDeep(item));
9
+ if (value !== null && typeof value === "object") {
10
+ const out = {};
11
+ for (const [k, v] of Object.entries(value)) {
12
+ out[k] = cloneDeep(v);
13
+ }
14
+ return out;
15
+ }
16
+ return value;
17
+ };
18
+
19
+ // src/utils/hash.ts
20
+ var isPlainObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
21
+ var toPrimitive = (v) => {
22
+ if (v === void 0 || v === null) return null;
23
+ if (typeof v === "bigint") return v.toString();
24
+ if (v instanceof Date) return `date:${v.getTime()}`;
25
+ if (typeof v === "object") {
26
+ const anyV = v;
27
+ if (typeof anyV.toHexString === "function") return `oid:${anyV.toHexString()}`;
28
+ if (anyV instanceof RegExp) return `re:${anyV.source}/${anyV.flags}`;
29
+ }
30
+ return v;
31
+ };
32
+ var stableStringify = (value) => {
33
+ const visit = (v) => {
34
+ if (Array.isArray(v)) return v.map(visit);
35
+ if (isPlainObject(v)) {
36
+ const out = {};
37
+ for (const key of Object.keys(v).sort()) out[key] = visit(v[key]);
38
+ return out;
39
+ }
40
+ return toPrimitive(v);
41
+ };
42
+ return JSON.stringify(visit(value));
43
+ };
44
+ var hashString = (input) => {
45
+ let hash = 5381;
46
+ for (let i = 0; i < input.length; i++) {
47
+ hash = (hash << 5) + hash + input.charCodeAt(i) | 0;
48
+ }
49
+ return (hash >>> 0).toString(36);
50
+ };
51
+ var stableHash = (value) => hashString(stableStringify(value));
52
+
53
+ // src/utils/logger.ts
54
+ var createNoopLogger = () => ({
55
+ debug: () => {
56
+ },
57
+ info: () => {
58
+ },
59
+ warn: () => {
60
+ },
61
+ error: () => {
62
+ }
63
+ });
64
+ var LEVEL_RANK = { debug: 10, info: 20, warn: 30, error: 40 };
65
+ var fmtObj = (obj) => {
66
+ try {
67
+ return JSON.stringify(obj);
68
+ } catch {
69
+ return String(obj);
70
+ }
71
+ };
72
+ var createConsoleLogger = (level = "info") => {
73
+ const threshold = LEVEL_RANK[level];
74
+ const make = (method) => {
75
+ const rank = LEVEL_RANK[method];
76
+ const fn = (a, b) => {
77
+ if (rank < threshold) return;
78
+ const obj = typeof a === "string" ? void 0 : a;
79
+ const msg = typeof a === "string" ? a : b;
80
+ const prefix = obj !== void 0 ? fmtObj(obj) : "";
81
+ if (msg && prefix) {
82
+ console[method](`[${method.toUpperCase()}]`, prefix, msg);
83
+ } else {
84
+ console[method](`[${method.toUpperCase()}]`, msg ?? prefix);
85
+ }
86
+ };
87
+ return fn;
88
+ };
89
+ return {
90
+ debug: make("debug"),
91
+ info: make("info"),
92
+ warn: make("warn"),
93
+ error: make("error")
94
+ };
95
+ };
96
+
97
+ // src/utils/lru.ts
98
+ var LRU = class {
99
+ max;
100
+ map = /* @__PURE__ */ new Map();
101
+ /** Number of entries evicted to make room for new ones (capacity evictions only). */
102
+ evictions = 0;
103
+ constructor(options) {
104
+ if (options.max <= 0) throw new Error("LRU max must be > 0");
105
+ this.max = options.max;
106
+ }
107
+ get size() {
108
+ return this.map.size;
109
+ }
110
+ get(key) {
111
+ const value = this.map.get(key);
112
+ if (value === void 0) return void 0;
113
+ this.map.delete(key);
114
+ this.map.set(key, value);
115
+ return value;
116
+ }
117
+ peek(key) {
118
+ return this.map.get(key);
119
+ }
120
+ has(key) {
121
+ return this.map.has(key);
122
+ }
123
+ set(key, value) {
124
+ if (this.map.has(key)) this.map.delete(key);
125
+ this.map.set(key, value);
126
+ if (this.map.size > this.max) {
127
+ const oldest = this.map.keys().next().value;
128
+ if (oldest !== void 0) {
129
+ this.map.delete(oldest);
130
+ this.evictions++;
131
+ }
132
+ }
133
+ return this;
134
+ }
135
+ delete(key) {
136
+ return this.map.delete(key);
137
+ }
138
+ clear() {
139
+ this.map.clear();
140
+ }
141
+ keys() {
142
+ return this.map.keys();
143
+ }
144
+ entries() {
145
+ return this.map.entries();
146
+ }
147
+ [Symbol.iterator]() {
148
+ return this.map.entries();
149
+ }
150
+ };
151
+
152
+ // src/utils/memoize.ts
153
+ var makeStore = (maxSize) => {
154
+ if (maxSize <= 0) {
155
+ const map = /* @__PURE__ */ new Map();
156
+ return {
157
+ get: (k) => map.get(k),
158
+ set: (k, v) => {
159
+ map.set(k, v);
160
+ },
161
+ has: (k) => map.has(k),
162
+ delete: (k) => {
163
+ map.delete(k);
164
+ }
165
+ };
166
+ }
167
+ return new LRU({ max: maxSize });
168
+ };
169
+ var createCachedFactory = (factory, options = {}) => {
170
+ const maxSize = options.maxSize ?? 100;
171
+ const cache = makeStore(maxSize);
172
+ return (key) => {
173
+ const existing = cache.get(key);
174
+ if (existing !== void 0) return existing;
175
+ const value = factory(key);
176
+ cache.set(key, value);
177
+ return value;
178
+ };
179
+ };
180
+ var createCachedAsyncFactory = (factory, options = {}) => {
181
+ const maxSize = options.maxSize ?? 100;
182
+ const cache = makeStore(maxSize);
183
+ const inFlight = /* @__PURE__ */ new Map();
184
+ return (key) => {
185
+ const inflight = inFlight.get(key);
186
+ if (inflight) return inflight;
187
+ const cached = cache.get(key);
188
+ if (cached) return cached;
189
+ const promise = factory(key);
190
+ inFlight.set(key, promise);
191
+ cache.set(key, promise);
192
+ promise.then(
193
+ () => {
194
+ inFlight.delete(key);
195
+ },
196
+ () => {
197
+ inFlight.delete(key);
198
+ if (cache.get(key) === promise) cache.delete(key);
199
+ }
200
+ );
201
+ return promise;
202
+ };
203
+ };
204
+
205
+ // src/utils/timeout.ts
206
+ var withTimeout = (promise, timeoutMs, label) => {
207
+ if (timeoutMs <= 0) return promise;
208
+ let timer;
209
+ const timeout = new Promise((_, reject) => {
210
+ timer = setTimeout(() => {
211
+ reject(new Error(`Operation timed out after ${timeoutMs}ms: ${label}`));
212
+ }, timeoutMs);
213
+ });
214
+ return Promise.race([promise, timeout]).finally(() => {
215
+ if (timer !== void 0) clearTimeout(timer);
216
+ });
217
+ };
218
+ var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
219
+ var sleepJittered = (ms, jitterMs = 1e3) => sleep(ms + Math.floor(Math.random() * Math.min(ms, jitterMs)));
220
+
221
+ export { LRU, cloneDeep, createCachedAsyncFactory, createCachedFactory, createConsoleLogger, createNoopLogger, hashString, sleep, sleepJittered, stableHash, stableStringify, withTimeout };
222
+ //# sourceMappingURL=utils.js.map
223
+ //# sourceMappingURL=utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/utils/clone.ts","../src/utils/hash.ts","../src/utils/logger.ts","../src/utils/lru.ts","../src/utils/memoize.ts","../src/utils/timeout.ts"],"names":[],"mappings":"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","file":"utils.js","sourcesContent":["import { ObjectId } from 'mongodb';\n\n/**\n * Deep-clone a value while preserving the BSON-ish types that\n * `JSON.parse(JSON.stringify())` would corrupt: `Date`, `ObjectId`, and\n * `RegExp`. Arrays and plain objects are copied recursively; primitives pass\n * through by reference. Used by the optional `clone` mode on `QueryCache` /\n * `HotCache` so a caller can't mutate a cached result and poison the shared\n * cache.\n */\nexport const cloneDeep = <T>(value: T): T => {\n if (value instanceof Date) return new Date(value.getTime()) as T;\n if (value instanceof ObjectId) return new ObjectId(value.toHexString()) as T;\n if (value instanceof RegExp) return new RegExp(value.source, value.flags) as T;\n if (Array.isArray(value)) return value.map((item) => cloneDeep(item)) as T;\n if (value !== null && typeof value === 'object') {\n const out: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(value as Record<string, unknown>)) {\n out[k] = cloneDeep(v);\n }\n return out as T;\n }\n return value;\n};\n","/**\n * Deterministic hashing utilities used for cache keys / in-flight dedup keys.\n * `stableStringify` produces an order-insensitive serialization so two logically\n * identical filters yield the same hash regardless of key insertion order.\n */\n\ntype Primitive = string | number | boolean | bigint | null | undefined;\n\nconst isPlainObject = (v: unknown): v is Record<string, unknown> =>\n typeof v === 'object' && v !== null && !Array.isArray(v);\n\nconst toPrimitive = (v: unknown): Primitive => {\n if (v === undefined || v === null) return null;\n if (typeof v === 'bigint') return v.toString();\n if (v instanceof Date) return `date:${v.getTime()}`;\n if (typeof v === 'object') {\n // Mongo driver ObjectId has toHexString(); RegExp serializes as a string.\n const anyV = v as { toHexString?: () => string; source?: string; flags?: string };\n if (typeof anyV.toHexString === 'function') return `oid:${anyV.toHexString()}`;\n if (anyV instanceof RegExp) return `re:${anyV.source}/${anyV.flags}`;\n }\n return v as Primitive;\n};\n\n/**\n * Stable stringify: recursively sorts object keys, coerces Date/ObjectId/RegExp\n * into primitive markers. Result is deterministic for a given logical value.\n */\nexport const stableStringify = (value: unknown): string => {\n const visit = (v: unknown): Primitive | Record<string, unknown> | unknown[] => {\n if (Array.isArray(v)) return v.map(visit);\n if (isPlainObject(v)) {\n const out: Record<string, unknown> = {};\n for (const key of Object.keys(v).sort()) out[key] = visit(v[key]);\n return out;\n }\n return toPrimitive(v);\n };\n return JSON.stringify(visit(value));\n};\n\n/** djb2 string hash → base-36 (fits comfortably in a double, no BigInt). */\nexport const hashString = (input: string): string => {\n let hash = 5381;\n for (let i = 0; i < input.length; i++) {\n hash = ((hash << 5) + hash + input.charCodeAt(i)) | 0;\n }\n return (hash >>> 0).toString(36);\n};\n\n/** Convenience: hash any value through a stable stringification. */\nexport const stableHash = (value: unknown): string => hashString(stableStringify(value));\n","/**\n * Logger abstraction. The ORM never logs directly — it accepts any logger that\n * satisfies `LoggerLike` (pino, a console wrapper, a test stub, ...). This keeps\n * the package free of logging dependencies while remaining observability-agnostic.\n */\nexport interface LogFn {\n (obj: Record<string, unknown>, msg?: string): void;\n (msg: string): void;\n}\n\nexport interface LoggerLike {\n debug: LogFn;\n info: LogFn;\n warn: LogFn;\n error: LogFn;\n}\n\n/** A logger that swallows everything. Useful in tests / perf runs. */\nexport const createNoopLogger = (): LoggerLike => ({\n debug: () => {},\n info: () => {},\n warn: () => {},\n error: () => {},\n});\n\nconst LEVELS = ['debug', 'info', 'warn', 'error'] as const;\nexport type LogLevel = (typeof LEVELS)[number];\n\nconst LEVEL_RANK: Record<LogLevel, number> = { debug: 10, info: 20, warn: 30, error: 40 };\n\nconst fmtObj = (obj: Record<string, unknown>): string => {\n try {\n return JSON.stringify(obj);\n } catch {\n return String(obj);\n }\n};\n\n/**\n * A tiny console logger matching the `LoggerLike` shape. `level` filters output.\n */\nexport const createConsoleLogger = (level: LogLevel = 'info'): LoggerLike => {\n const threshold = LEVEL_RANK[level];\n const make = (method: 'debug' | 'info' | 'warn' | 'error') => {\n const rank = LEVEL_RANK[method];\n const fn: LogFn = (a: unknown, b?: string) => {\n if (rank < threshold) return;\n const obj = typeof a === 'string' ? undefined : (a as Record<string, unknown>);\n const msg = typeof a === 'string' ? a : b;\n const prefix = obj !== undefined ? fmtObj(obj) : '';\n if (msg && prefix) {\n // eslint-disable-next-line no-console\n console[method](`[${method.toUpperCase()}]`, prefix, msg);\n } else {\n // eslint-disable-next-line no-console\n console[method](`[${method.toUpperCase()}]`, (msg ?? prefix) as string);\n }\n };\n return fn;\n };\n return {\n debug: make('debug'),\n info: make('info'),\n warn: make('warn'),\n error: make('error'),\n };\n};\n","/**\n * Minimal dependency-free LRU cache backed by a Map (recency = insertion order).\n * get() re-inserts the entry so the most-recently-used item is always last.\n */\nexport interface LRUOptions {\n /** Maximum number of entries before the least-recently-used item is evicted. Must be > 0. */\n max: number;\n}\n\nexport class LRU<K, V> {\n readonly max: number;\n private map = new Map<K, V>();\n /** Number of entries evicted to make room for new ones (capacity evictions only). */\n evictions = 0;\n\n constructor(options: LRUOptions) {\n if (options.max <= 0) throw new Error('LRU max must be > 0');\n this.max = options.max;\n }\n\n get size(): number {\n return this.map.size;\n }\n\n get(key: K): V | undefined {\n const value = this.map.get(key);\n if (value === undefined) return undefined;\n // Refresh recency.\n this.map.delete(key);\n this.map.set(key, value);\n return value;\n }\n\n peek(key: K): V | undefined {\n return this.map.get(key);\n }\n\n has(key: K): boolean {\n return this.map.has(key);\n }\n\n set(key: K, value: V): this {\n if (this.map.has(key)) this.map.delete(key);\n this.map.set(key, value);\n if (this.map.size > this.max) {\n const oldest = this.map.keys().next().value as K | undefined;\n if (oldest !== undefined) {\n this.map.delete(oldest);\n this.evictions++;\n }\n }\n return this;\n }\n\n delete(key: K): boolean {\n return this.map.delete(key);\n }\n\n clear(): void {\n this.map.clear();\n }\n\n keys(): IterableIterator<K> {\n return this.map.keys();\n }\n\n entries(): IterableIterator<[K, V]> {\n return this.map.entries();\n }\n\n [Symbol.iterator](): IterableIterator<[K, V]> {\n return this.map.entries();\n }\n}\n","/**\n * Generic keyed factory caches (memoization). Unlike the ORM's `QueryCache`\n * (per-collection, TTL, invalidation-aware) and `HotCache` (read-through,\n * event/timer-refreshed), these are dependency-free building blocks for\n * memoizing any expensive computation by key — including in-flight dedup so a\n * keyed factory never runs twice concurrently.\n */\nimport { LRU } from './lru.ts';\n\nexport interface CacheOptions {\n /** Max cached entries. `0` or negative → unbounded Map (default 100). */\n maxSize?: number;\n}\n\ntype BackingStore<K, V> = {\n get(k: K): V | undefined;\n set(k: K, v: V): void;\n has(k: K): boolean;\n delete(k: K): void;\n};\n\nconst makeStore = <K, V>(maxSize: number): BackingStore<K, V> => {\n if (maxSize <= 0) {\n const map = new Map<K, V>();\n return {\n get: (k) => map.get(k),\n set: (k, v) => {\n map.set(k, v);\n },\n has: (k) => map.has(k),\n delete: (k) => {\n map.delete(k);\n },\n };\n }\n return new LRU<K, V>({ max: maxSize });\n};\n\n/**\n * Synchronous keyed factory cache: one value per key, LRU-bounded.\n * Used for cheap singleton factories (connection pools, resolvers, ...).\n */\nexport const createCachedFactory = <K, V>(\n factory: (key: K) => V,\n options: CacheOptions = {},\n): ((key: K) => V) => {\n const maxSize = options.maxSize ?? 100;\n const cache = makeStore<K, V>(maxSize);\n return (key: K): V => {\n const existing = cache.get(key);\n if (existing !== undefined) return existing;\n const value = factory(key);\n cache.set(key, value);\n return value;\n };\n};\n\n/**\n * Async keyed factory cache with **in-flight deduplication**: concurrent calls\n * with the same key share a single Promise so the factory runs only once.\n * On settle (success or failure) the in-flight entry is removed.\n */\nexport const createCachedAsyncFactory = <K, V>(\n factory: (key: K) => Promise<V>,\n options: CacheOptions = {},\n): ((key: K) => Promise<V>) => {\n const maxSize = options.maxSize ?? 100;\n const cache = makeStore<K, Promise<V>>(maxSize);\n const inFlight = new Map<K, Promise<V>>();\n\n return (key: K): Promise<V> => {\n const inflight = inFlight.get(key);\n if (inflight) return inflight;\n\n const cached = cache.get(key);\n if (cached) return cached;\n\n const promise = factory(key);\n inFlight.set(key, promise);\n cache.set(key, promise);\n promise.then(\n () => {\n inFlight.delete(key);\n },\n () => {\n inFlight.delete(key);\n // Never cache a failure — evict (identity-guarded) so the next call\n // retries instead of re-surfacing the same rejected promise.\n if (cache.get(key) === promise) cache.delete(key);\n },\n );\n return promise;\n };\n};\n","/**\n * Race a promise against a timeout. A `timeoutMs` <= 0 disables the timer and\n * returns the original promise untouched (zero-overhead fast path).\n */\nexport const withTimeout = <T>(\n promise: Promise<T>,\n timeoutMs: number,\n label: string,\n): Promise<T> => {\n if (timeoutMs <= 0) return promise;\n let timer: ReturnType<typeof setTimeout> | undefined;\n const timeout = new Promise<never>((_, reject) => {\n timer = setTimeout(() => {\n reject(new Error(`Operation timed out after ${timeoutMs}ms: ${label}`));\n }, timeoutMs);\n });\n return Promise.race([promise, timeout]).finally(() => {\n if (timer !== undefined) clearTimeout(timer);\n });\n};\n\n/** Sleep helper used by retry backoff. */\nexport const sleep = (ms: number): Promise<void> => new Promise((r) => setTimeout(r, ms));\n\n/**\n * Sleep for `ms` plus random jitter (0..jitterMs, capped at `ms`) so retry\n * loops don't resynchronize into a thundering herd after a replica failover.\n */\nexport const sleepJittered = (ms: number, jitterMs = 1000): Promise<void> =>\n sleep(ms + Math.floor(Math.random() * Math.min(ms, jitterMs)));\n"]}
package/package.json ADDED
@@ -0,0 +1,83 @@
1
+ {
2
+ "name": "@ignex/ninox",
3
+ "version": "0.1.1",
4
+ "description": "Schema-first MongoDB ORM with DataLoader-batched relations, query caching, in-flight dedup, and $facet pagination",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "sideEffects": false,
8
+ "engines": {
9
+ "node": ">=18.17.0",
10
+ "bun": ">=1.0.0"
11
+ },
12
+ "files": [
13
+ "dist"
14
+ ],
15
+ "main": "./dist/index.js",
16
+ "module": "./dist/index.js",
17
+ "types": "./dist/index.d.ts",
18
+ "exports": {
19
+ ".": {
20
+ "types": "./dist/index.d.ts",
21
+ "import": "./dist/index.js",
22
+ "default": "./dist/index.js"
23
+ },
24
+ "./utils": {
25
+ "types": "./dist/utils.d.ts",
26
+ "import": "./dist/utils.js",
27
+ "default": "./dist/utils.js"
28
+ },
29
+ "./package.json": "./package.json"
30
+ },
31
+ "scripts": {
32
+ "build": "tsup",
33
+ "dev": "bun run index.ts",
34
+ "start": "bun run index.ts",
35
+ "test": "bun test",
36
+ "test:coverage": "bun test --coverage",
37
+ "test:types": "tsc --noEmit",
38
+ "bench": "bun run bench/run-bench.ts",
39
+ "check:api": "bun run scripts/check-api.ts",
40
+ "typecheck": "tsc --noEmit",
41
+ "lint": "biome check .",
42
+ "lint:fix": "biome check --write .",
43
+ "format": "biome format --write .",
44
+ "prepublishOnly": "bun run typecheck && bun run lint && bun run build",
45
+ "deploy": "bun run scripts/deploy.ts",
46
+ "deploy:dry": "bun run scripts/deploy.ts --dry-run"
47
+ },
48
+ "devDependencies": {
49
+ "@biomejs/biome": "^2.0.0",
50
+ "@types/bun": "latest",
51
+ "tsup": "^8.0.0",
52
+ "typescript": "^6.0.3"
53
+ },
54
+ "peerDependencies": {
55
+ "typescript": ">=5.5.0"
56
+ },
57
+ "peerDependenciesMeta": {
58
+ "typescript": {
59
+ "optional": true
60
+ }
61
+ },
62
+ "dependencies": {
63
+ "mongodb": "^7.5.0"
64
+ },
65
+ "keywords": [
66
+ "mongodb",
67
+ "orm",
68
+ "typescript",
69
+ "schema",
70
+ "query-builder",
71
+ "dataloader",
72
+ "cache",
73
+ "aggregation",
74
+ "json-schema"
75
+ ],
76
+ "repository": {
77
+ "type": "git",
78
+ "url": "https://github.com/ignex/ninox.git"
79
+ },
80
+ "publishConfig": {
81
+ "access": "public"
82
+ }
83
+ }