@open-mercato/shared 0.10.1-develop.7576.1.21d5b50435 → 0.10.1-develop.7577.1.477f2486e5
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.
- package/.turbo/turbo-build.log +1 -1
- package/AGENTS.md +4 -4
- package/dist/lib/auth/server.js +1 -1
- package/dist/lib/auth/server.js.map +2 -2
- package/dist/lib/bootstrap/dynamicLoader.js +46 -6
- package/dist/lib/bootstrap/dynamicLoader.js.map +2 -2
- package/dist/lib/bootstrap/factory.js +4 -3
- package/dist/lib/bootstrap/factory.js.map +2 -2
- package/dist/lib/bootstrap/next-subpath-resolve-hook-worker.js +6 -0
- package/dist/lib/bootstrap/next-subpath-resolve-hook-worker.js.map +7 -0
- package/dist/lib/bootstrap/next-subpath-resolve-hook.js +62 -0
- package/dist/lib/bootstrap/next-subpath-resolve-hook.js.map +7 -0
- package/dist/lib/catalog-visibility/canonicalizeEffectiveScope.js +33 -0
- package/dist/lib/catalog-visibility/canonicalizeEffectiveScope.js.map +7 -0
- package/dist/lib/catalog-visibility/hashEffectiveScope.js +9 -0
- package/dist/lib/catalog-visibility/hashEffectiveScope.js.map +7 -0
- package/dist/lib/catalog-visibility/index.js +4 -0
- package/dist/lib/catalog-visibility/index.js.map +2 -2
- package/dist/lib/commands/command-bus.js +45 -13
- package/dist/lib/commands/command-bus.js.map +2 -2
- package/dist/lib/commands/types.js.map +2 -2
- package/dist/lib/crud/exporters.js +1 -1
- package/dist/lib/crud/exporters.js.map +2 -2
- package/dist/lib/crud/ids.js +13 -1
- package/dist/lib/crud/ids.js.map +2 -2
- package/dist/lib/db/advisoryLock.js +172 -0
- package/dist/lib/db/advisoryLock.js.map +7 -0
- package/dist/lib/db/pg-errors.js +14 -0
- package/dist/lib/db/pg-errors.js.map +2 -2
- package/dist/lib/localization/index.js +9 -1
- package/dist/lib/localization/index.js.map +2 -2
- package/dist/lib/localization/translatable-fields.js +26 -0
- package/dist/lib/localization/translatable-fields.js.map +2 -2
- package/dist/lib/number.js +6 -0
- package/dist/lib/number.js.map +2 -2
- package/dist/lib/openapi/generator.js +84 -67
- package/dist/lib/openapi/generator.js.map +2 -2
- package/dist/lib/query/engine.js +45 -10
- package/dist/lib/query/engine.js.map +2 -2
- package/dist/lib/query/join-utils.js +1 -1
- package/dist/lib/query/join-utils.js.map +2 -2
- package/dist/lib/query/overlap.js +58 -0
- package/dist/lib/query/overlap.js.map +7 -0
- package/dist/lib/query/types.js.map +2 -2
- package/dist/lib/url.js +24 -0
- package/dist/lib/url.js.map +2 -2
- package/dist/lib/version.js +1 -1
- package/dist/lib/version.js.map +1 -1
- package/dist/lib/webhooks/body.js +7 -3
- package/dist/lib/webhooks/body.js.map +2 -2
- package/dist/lib/webhooks/index.js +2 -0
- package/dist/lib/webhooks/index.js.map +2 -2
- package/dist/modules/integrations/types.js.map +2 -2
- package/dist/modules/notifications/safeHref.js +20 -0
- package/dist/modules/notifications/safeHref.js.map +7 -0
- package/dist/modules/notifications/types.js +4 -0
- package/dist/modules/notifications/types.js.map +3 -3
- package/dist/modules/payment_gateways/types.js +14 -1
- package/dist/modules/payment_gateways/types.js.map +2 -2
- package/dist/modules/registry.js.map +2 -2
- package/dist/modules/search.js.map +2 -2
- package/dist/modules/widgets/injection-loader.js +14 -3
- package/dist/modules/widgets/injection-loader.js.map +2 -2
- package/package.json +9 -2
- package/src/lib/__tests__/number.test.ts +24 -1
- package/src/lib/__tests__/url.cross-site.test.ts +140 -0
- package/src/lib/auth/__tests__/server.test.ts +14 -0
- package/src/lib/auth/server.ts +1 -1
- package/src/lib/bootstrap/__tests__/dynamicLoader.appDi.test.ts +2 -2
- package/src/lib/bootstrap/__tests__/dynamicLoader.appModuleOverrides.test.ts +2 -2
- package/src/lib/bootstrap/__tests__/dynamicLoader.cacheRecovery.test.ts +2 -2
- package/src/lib/bootstrap/__tests__/dynamicLoader.commandInterceptors.test.ts +2 -2
- package/src/lib/bootstrap/__tests__/dynamicLoader.externalSubpath.test.ts +141 -0
- package/src/lib/bootstrap/__tests__/dynamicLoader.tsconfig.test.ts +2 -2
- package/src/lib/bootstrap/__tests__/factory.test.ts +18 -6
- package/src/lib/bootstrap/__tests__/next-subpath-resolve-hook.test.ts +174 -0
- package/src/lib/bootstrap/dynamicLoader.ts +63 -5
- package/src/lib/bootstrap/factory.ts +8 -3
- package/src/lib/bootstrap/next-subpath-resolve-hook-worker.ts +3 -0
- package/src/lib/bootstrap/next-subpath-resolve-hook.ts +130 -0
- package/src/lib/bootstrap/types.ts +5 -1
- package/src/lib/catalog-visibility/__tests__/hashEffectiveScope.test.ts +121 -0
- package/src/lib/catalog-visibility/canonicalizeEffectiveScope.ts +41 -0
- package/src/lib/catalog-visibility/hashEffectiveScope.ts +8 -0
- package/src/lib/catalog-visibility/index.ts +2 -0
- package/src/lib/commands/__tests__/command-bus.test.ts +100 -1
- package/src/lib/commands/command-bus.ts +42 -10
- package/src/lib/commands/command-interceptor.ts +4 -0
- package/src/lib/commands/types.ts +14 -0
- package/src/lib/crud/__tests__/exporters.test.ts +18 -0
- package/src/lib/crud/__tests__/ids.test.ts +34 -1
- package/src/lib/crud/exporters.ts +1 -1
- package/src/lib/crud/ids.ts +24 -0
- package/src/lib/db/__tests__/advisoryLock.integration.test.ts +343 -0
- package/src/lib/db/__tests__/advisoryLock.test.ts +564 -0
- package/src/lib/db/__tests__/pg-errors.test.ts +68 -1
- package/src/lib/db/advisoryLock.ts +271 -0
- package/src/lib/db/pg-errors.ts +35 -0
- package/src/lib/encryption/__tests__/tenantDataEncryptionService.test.ts +23 -0
- package/src/lib/localization/__tests__/translatable-fields.test.ts +17 -0
- package/src/lib/localization/index.ts +8 -1
- package/src/lib/localization/translatable-fields.ts +46 -0
- package/src/lib/number.ts +13 -0
- package/src/lib/openapi/__tests__/optional-path-params.test.ts +102 -0
- package/src/lib/openapi/generator.ts +106 -77
- package/src/lib/query/__tests__/empty-ids-filter.pg.test.ts +89 -0
- package/src/lib/query/__tests__/engine.empty-in-filter.test.ts +79 -0
- package/src/lib/query/__tests__/overlap.test.ts +219 -0
- package/src/lib/query/engine.ts +58 -8
- package/src/lib/query/join-utils.ts +1 -1
- package/src/lib/query/overlap.ts +106 -0
- package/src/lib/query/types.ts +13 -1
- package/src/lib/url.ts +30 -0
- package/src/lib/webhooks/__tests__/body.test.ts +48 -0
- package/src/lib/webhooks/body.ts +11 -4
- package/src/lib/webhooks/index.ts +1 -0
- package/src/modules/integrations/types.ts +6 -0
- package/src/modules/notifications/safeHref.ts +19 -0
- package/src/modules/notifications/types.ts +7 -0
- package/src/modules/payment_gateways/__tests__/types.test.ts +44 -0
- package/src/modules/payment_gateways/types.ts +52 -6
- package/src/modules/registry.ts +5 -0
- package/src/modules/search.ts +39 -0
- package/src/modules/widgets/__tests__/injection-loader.partitioned-registration.test.ts +78 -0
- package/src/modules/widgets/injection-loader.ts +33 -3
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { calculateBackoffDelayMs } from "../delivery/retry.js";
|
|
2
|
+
import { isTransientLockDbError } from "./pg-errors.js";
|
|
3
|
+
const ADVISORY_LOCK_UNAVAILABLE_BRAND = /* @__PURE__ */ Symbol.for("@open-mercato/shared:AdvisoryLockUnavailableError");
|
|
4
|
+
const ADVISORY_LOCK_UNAVAILABLE_MESSAGES = {
|
|
5
|
+
deadline: "[internal] Advisory lock not acquired before the wait deadline",
|
|
6
|
+
transient_db: "[internal] Advisory lock transaction failed with a transient database error",
|
|
7
|
+
commit_unknown: "[internal] Advisory lock transaction failed after its callback returned; the commit outcome is unknown"
|
|
8
|
+
};
|
|
9
|
+
class AdvisoryLockUnavailableError extends Error {
|
|
10
|
+
static [Symbol.hasInstance](value) {
|
|
11
|
+
return typeof value === "object" && value !== null && Reflect.get(value, ADVISORY_LOCK_UNAVAILABLE_BRAND) === true;
|
|
12
|
+
}
|
|
13
|
+
constructor(reason, key, cause) {
|
|
14
|
+
super(ADVISORY_LOCK_UNAVAILABLE_MESSAGES[reason], cause === void 0 ? void 0 : { cause });
|
|
15
|
+
Object.defineProperty(this, ADVISORY_LOCK_UNAVAILABLE_BRAND, { value: true, enumerable: false });
|
|
16
|
+
this.name = "AdvisoryLockUnavailableError";
|
|
17
|
+
this.reason = reason;
|
|
18
|
+
this.key = key;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
const DEFAULT_WAIT_DEADLINE_MS = 15e3;
|
|
22
|
+
const BACKOFF_ATTEMPT_CAP = 5;
|
|
23
|
+
const BACKOFF_OPTIONS = { baseDelayMs: 50, maxJitterMs: 25 };
|
|
24
|
+
const KEY_NAMESPACE_PATTERN = /^[a-z][a-z0-9_]*$/;
|
|
25
|
+
const ADVISORY_LOCK_SQL = "select pg_try_advisory_xact_lock(hashtextextended(?, 0)) as locked";
|
|
26
|
+
const LOCK_FORK_OPTIONS = {
|
|
27
|
+
disableContextResolution: true,
|
|
28
|
+
clear: true,
|
|
29
|
+
useContext: false,
|
|
30
|
+
disableTransactions: false,
|
|
31
|
+
cloneEventManager: true
|
|
32
|
+
};
|
|
33
|
+
const GLOBAL_SLOTS_KEY = "__openMercatoAdvisoryLockSlots__";
|
|
34
|
+
const NOT_ACQUIRED = { acquired: false };
|
|
35
|
+
const releaseNothing = () => void 0;
|
|
36
|
+
function getSlotRegistry() {
|
|
37
|
+
const globals = globalThis;
|
|
38
|
+
const existing = globals[GLOBAL_SLOTS_KEY];
|
|
39
|
+
if (existing instanceof Map) return existing;
|
|
40
|
+
const created = /* @__PURE__ */ new Map();
|
|
41
|
+
globals[GLOBAL_SLOTS_KEY] = created;
|
|
42
|
+
return created;
|
|
43
|
+
}
|
|
44
|
+
function getNamespaceSlots(namespace) {
|
|
45
|
+
const registry = getSlotRegistry();
|
|
46
|
+
const existing = registry.get(namespace);
|
|
47
|
+
if (existing) return existing;
|
|
48
|
+
const created = { held: 0, waiters: [] };
|
|
49
|
+
registry.set(namespace, created);
|
|
50
|
+
return created;
|
|
51
|
+
}
|
|
52
|
+
function handOffSlots(slots) {
|
|
53
|
+
let index = 0;
|
|
54
|
+
while (index < slots.waiters.length) {
|
|
55
|
+
const waiter = slots.waiters[index];
|
|
56
|
+
if (slots.held >= waiter.limit) {
|
|
57
|
+
index += 1;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
slots.waiters.splice(index, 1);
|
|
61
|
+
slots.held += 1;
|
|
62
|
+
waiter.grant();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function createSlotRelease(slots) {
|
|
66
|
+
let released = false;
|
|
67
|
+
return () => {
|
|
68
|
+
if (released) return;
|
|
69
|
+
released = true;
|
|
70
|
+
slots.held -= 1;
|
|
71
|
+
handOffSlots(slots);
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
function acquireSlot(namespace, limit, key, deadlineAt) {
|
|
75
|
+
const slots = getNamespaceSlots(namespace);
|
|
76
|
+
if (slots.held < limit) {
|
|
77
|
+
slots.held += 1;
|
|
78
|
+
return Promise.resolve(createSlotRelease(slots));
|
|
79
|
+
}
|
|
80
|
+
const remainingMs = deadlineAt - Date.now();
|
|
81
|
+
if (remainingMs <= 0) return Promise.reject(new AdvisoryLockUnavailableError("deadline", key));
|
|
82
|
+
return new Promise((resolve, reject) => {
|
|
83
|
+
const waiter = {
|
|
84
|
+
limit,
|
|
85
|
+
grant: () => {
|
|
86
|
+
clearTimeout(deadlineTimer);
|
|
87
|
+
resolve(createSlotRelease(slots));
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
const deadlineTimer = setTimeout(() => {
|
|
91
|
+
const position = slots.waiters.indexOf(waiter);
|
|
92
|
+
if (position >= 0) slots.waiters.splice(position, 1);
|
|
93
|
+
reject(new AdvisoryLockUnavailableError("deadline", key));
|
|
94
|
+
}, remainingMs);
|
|
95
|
+
slots.waiters.push(waiter);
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
function parseKeyNamespace(key) {
|
|
99
|
+
const separator = typeof key === "string" ? key.indexOf(":") : -1;
|
|
100
|
+
if (typeof key === "string" && separator > 0 && separator < key.length - 1) {
|
|
101
|
+
const namespace = key.slice(0, separator);
|
|
102
|
+
if (KEY_NAMESPACE_PATTERN.test(namespace)) return namespace;
|
|
103
|
+
}
|
|
104
|
+
throw new TypeError('[internal] Advisory lock key must be "<namespace>:<parts>" with a namespace matching ^[a-z][a-z0-9_]*$');
|
|
105
|
+
}
|
|
106
|
+
function resolveWaitDeadlineMs(value) {
|
|
107
|
+
if (value === void 0) return DEFAULT_WAIT_DEADLINE_MS;
|
|
108
|
+
if (Number.isFinite(value) && value >= 0) return value;
|
|
109
|
+
throw new TypeError("[internal] Advisory lock waitDeadlineMs must be a finite number of at least 0");
|
|
110
|
+
}
|
|
111
|
+
function resolveMaxConcurrentHolders(value) {
|
|
112
|
+
if (value === void 0) return null;
|
|
113
|
+
if (Number.isInteger(value) && value > 0) return value;
|
|
114
|
+
throw new TypeError("[internal] Advisory lock maxConcurrentHolders must be a positive integer");
|
|
115
|
+
}
|
|
116
|
+
function sleep(delayMs) {
|
|
117
|
+
return new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
118
|
+
}
|
|
119
|
+
async function attemptLock(lockEm, key, fn) {
|
|
120
|
+
const callback = { thrown: false, returned: false, error: void 0 };
|
|
121
|
+
try {
|
|
122
|
+
return await lockEm.transactional(async (txEm) => {
|
|
123
|
+
const rows = await txEm.execute(ADVISORY_LOCK_SQL, [key]);
|
|
124
|
+
if (rows[0]?.locked !== true) return NOT_ACQUIRED;
|
|
125
|
+
try {
|
|
126
|
+
const value = await fn(txEm);
|
|
127
|
+
callback.returned = true;
|
|
128
|
+
return { acquired: true, value };
|
|
129
|
+
} catch (error) {
|
|
130
|
+
callback.thrown = true;
|
|
131
|
+
callback.error = error;
|
|
132
|
+
throw error;
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
} catch (error) {
|
|
136
|
+
if (callback.thrown) throw callback.error;
|
|
137
|
+
if (isTransientLockDbError(error)) {
|
|
138
|
+
throw new AdvisoryLockUnavailableError(callback.returned ? "commit_unknown" : "transient_db", key, error);
|
|
139
|
+
}
|
|
140
|
+
throw error;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
async function withAdvisoryXactLock(em, key, fn, options = {}) {
|
|
144
|
+
const namespace = parseKeyNamespace(key);
|
|
145
|
+
const waitDeadlineMs = resolveWaitDeadlineMs(options.waitDeadlineMs);
|
|
146
|
+
const maxConcurrentHolders = resolveMaxConcurrentHolders(options.maxConcurrentHolders);
|
|
147
|
+
const deadlineAt = Date.now() + waitDeadlineMs;
|
|
148
|
+
const lockEm = em.fork(LOCK_FORK_OPTIONS);
|
|
149
|
+
for (let attempt = 1; ; attempt += 1) {
|
|
150
|
+
const releaseSlot = maxConcurrentHolders === null ? releaseNothing : await acquireSlot(namespace, maxConcurrentHolders, key, deadlineAt);
|
|
151
|
+
let outcome;
|
|
152
|
+
try {
|
|
153
|
+
outcome = await attemptLock(lockEm, key, fn);
|
|
154
|
+
} finally {
|
|
155
|
+
releaseSlot();
|
|
156
|
+
}
|
|
157
|
+
if (outcome.acquired) return outcome.value;
|
|
158
|
+
const remainingMs = deadlineAt - Date.now();
|
|
159
|
+
if (remainingMs <= 0) throw new AdvisoryLockUnavailableError("deadline", key);
|
|
160
|
+
await sleep(Math.min(calculateBackoffDelayMs(Math.min(attempt, BACKOFF_ATTEMPT_CAP), BACKOFF_OPTIONS), remainingMs));
|
|
161
|
+
if (options.onWait) {
|
|
162
|
+
const waitResult = await options.onWait();
|
|
163
|
+
if (waitResult.done) return waitResult.value;
|
|
164
|
+
}
|
|
165
|
+
if (Date.now() >= deadlineAt) throw new AdvisoryLockUnavailableError("deadline", key);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
export {
|
|
169
|
+
AdvisoryLockUnavailableError,
|
|
170
|
+
withAdvisoryXactLock
|
|
171
|
+
};
|
|
172
|
+
//# sourceMappingURL=advisoryLock.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/lib/db/advisoryLock.ts"],
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { calculateBackoffDelayMs } from '../delivery/retry'\nimport { isTransientLockDbError } from './pg-errors'\n\n/** @internal */\nexport type AdvisoryLockWaitResult<T> = { done: true; value: T } | { done: false }\n\nexport type AdvisoryLockOptions<T> = {\n /** Total waiting budget from the call start. Default 15_000. */\n waitDeadlineMs?: number\n /** @internal Called after each failed attempt, with no connection held; `done: true` short-circuits. */\n onWait?: () => Promise<AdvisoryLockWaitResult<T>>\n /** Per process and key namespace: at most this many lock transactions at once (attempts included).\n * Waiting for a slot holds no connection and counts against `waitDeadlineMs`. Default: unlimited. */\n maxConcurrentHolders?: number\n}\n\nconst ADVISORY_LOCK_UNAVAILABLE_BRAND = Symbol.for('@open-mercato/shared:AdvisoryLockUnavailableError')\n\n/**\n * The helper's own work failed with an error `isTransientLockDbError` accepts (kept as `cause`), or\n * the wait ran out. `deadline`: the wait budget ran out (contention or no free holder slot), so `fn`\n * never ran. `transient_db`: the begin or the lock query failed, so `fn` never ran. `commit_unknown`:\n * `fn` returned and the flush or commit after it failed, so its writes may or may not be committed.\n * `reason` is additive-only; branch on it, never on the message.\n */\nexport type AdvisoryLockUnavailableReason = 'deadline' | 'transient_db' | 'commit_unknown'\n\nconst ADVISORY_LOCK_UNAVAILABLE_MESSAGES: Record<AdvisoryLockUnavailableReason, string> = {\n deadline: '[internal] Advisory lock not acquired before the wait deadline',\n transient_db: '[internal] Advisory lock transaction failed with a transient database error',\n commit_unknown: '[internal] Advisory lock transaction failed after its callback returned; the commit outcome is unknown',\n}\n\nexport class AdvisoryLockUnavailableError extends Error {\n static [Symbol.hasInstance](value: unknown): boolean {\n return typeof value === 'object' && value !== null && Reflect.get(value, ADVISORY_LOCK_UNAVAILABLE_BRAND) === true\n }\n\n readonly reason: AdvisoryLockUnavailableReason\n readonly key: string\n constructor(reason: AdvisoryLockUnavailableReason, key: string, cause?: unknown) {\n super(ADVISORY_LOCK_UNAVAILABLE_MESSAGES[reason], cause === undefined ? undefined : { cause })\n Object.defineProperty(this, ADVISORY_LOCK_UNAVAILABLE_BRAND, { value: true, enumerable: false })\n this.name = 'AdvisoryLockUnavailableError'\n this.reason = reason\n this.key = key\n }\n}\n\nconst DEFAULT_WAIT_DEADLINE_MS = 15_000\nconst BACKOFF_ATTEMPT_CAP = 5\nconst BACKOFF_OPTIONS = { baseDelayMs: 50, maxJitterMs: 25 }\nconst KEY_NAMESPACE_PATTERN = /^[a-z][a-z0-9_]*$/\nconst ADVISORY_LOCK_SQL = 'select pg_try_advisory_xact_lock(hashtextextended(?, 0)) as locked'\n\n/**\n * `disableContextResolution` keeps the fork off an outer transaction resolved from the\n * async context, `useContext: false` keeps the fork's own transaction from joining one\n * later, `disableTransactions: false` keeps a source EntityManager with transactions disabled\n * from turning the lock into a statement-scoped no-op, and the cloned event manager carries\n * the request's subscribers (tenant field encryption among them) into the lock transaction.\n */\nconst LOCK_FORK_OPTIONS = {\n disableContextResolution: true,\n clear: true,\n useContext: false,\n disableTransactions: false,\n cloneEventManager: true,\n}\n\n/**\n * Holder slots live on `globalThis` so duplicated module instances in one process (dev\n * HMR, standalone server chunks) count against one limit. The map is created once and\n * never replaced; each namespace entry is created on first use and kept.\n */\nconst GLOBAL_SLOTS_KEY = '__openMercatoAdvisoryLockSlots__'\n\ntype SlotWaiter = {\n limit: number\n grant: () => void\n}\n\ntype NamespaceSlots = {\n held: number\n waiters: SlotWaiter[]\n}\n\ntype SlotRelease = () => void\n\ntype AttemptOutcome<T> = { acquired: true; value: T } | { acquired: false }\n\nconst NOT_ACQUIRED: AttemptOutcome<never> = { acquired: false }\n\nconst releaseNothing: SlotRelease = () => undefined\n\nfunction getSlotRegistry(): Map<string, NamespaceSlots> {\n const globals = globalThis as Record<string, unknown>\n const existing = globals[GLOBAL_SLOTS_KEY]\n if (existing instanceof Map) return existing as Map<string, NamespaceSlots>\n const created = new Map<string, NamespaceSlots>()\n globals[GLOBAL_SLOTS_KEY] = created\n return created\n}\n\nfunction getNamespaceSlots(namespace: string): NamespaceSlots {\n const registry = getSlotRegistry()\n const existing = registry.get(namespace)\n if (existing) return existing\n const created: NamespaceSlots = { held: 0, waiters: [] }\n registry.set(namespace, created)\n return created\n}\n\nfunction handOffSlots(slots: NamespaceSlots): void {\n let index = 0\n while (index < slots.waiters.length) {\n const waiter = slots.waiters[index]\n if (slots.held >= waiter.limit) {\n index += 1\n continue\n }\n slots.waiters.splice(index, 1)\n slots.held += 1\n waiter.grant()\n }\n}\n\nfunction createSlotRelease(slots: NamespaceSlots): SlotRelease {\n let released = false\n return () => {\n if (released) return\n released = true\n slots.held -= 1\n handOffSlots(slots)\n }\n}\n\nfunction acquireSlot(namespace: string, limit: number, key: string, deadlineAt: number): Promise<SlotRelease> {\n const slots = getNamespaceSlots(namespace)\n if (slots.held < limit) {\n slots.held += 1\n return Promise.resolve(createSlotRelease(slots))\n }\n const remainingMs = deadlineAt - Date.now()\n if (remainingMs <= 0) return Promise.reject(new AdvisoryLockUnavailableError('deadline', key))\n return new Promise<SlotRelease>((resolve, reject) => {\n const waiter: SlotWaiter = {\n limit,\n grant: () => {\n clearTimeout(deadlineTimer)\n resolve(createSlotRelease(slots))\n },\n }\n const deadlineTimer = setTimeout(() => {\n const position = slots.waiters.indexOf(waiter)\n if (position >= 0) slots.waiters.splice(position, 1)\n reject(new AdvisoryLockUnavailableError('deadline', key))\n }, remainingMs)\n slots.waiters.push(waiter)\n })\n}\n\nfunction parseKeyNamespace(key: unknown): string {\n const separator = typeof key === 'string' ? key.indexOf(':') : -1\n if (typeof key === 'string' && separator > 0 && separator < key.length - 1) {\n const namespace = key.slice(0, separator)\n if (KEY_NAMESPACE_PATTERN.test(namespace)) return namespace\n }\n throw new TypeError('[internal] Advisory lock key must be \"<namespace>:<parts>\" with a namespace matching ^[a-z][a-z0-9_]*$')\n}\n\nfunction resolveWaitDeadlineMs(value: number | undefined): number {\n if (value === undefined) return DEFAULT_WAIT_DEADLINE_MS\n if (Number.isFinite(value) && value >= 0) return value\n throw new TypeError('[internal] Advisory lock waitDeadlineMs must be a finite number of at least 0')\n}\n\nfunction resolveMaxConcurrentHolders(value: number | undefined): number | null {\n if (value === undefined) return null\n if (Number.isInteger(value) && value > 0) return value\n throw new TypeError('[internal] Advisory lock maxConcurrentHolders must be a positive integer')\n}\n\nfunction sleep(delayMs: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, delayMs))\n}\n\nasync function attemptLock<T>(\n lockEm: EntityManager,\n key: string,\n fn: (txEm: EntityManager) => Promise<T>,\n): Promise<AttemptOutcome<T>> {\n const callback: { thrown: boolean; returned: boolean; error: unknown } = { thrown: false, returned: false, error: undefined }\n try {\n return await lockEm.transactional(async (txEm): Promise<AttemptOutcome<T>> => {\n const rows = await txEm.execute<Array<{ locked: boolean }>>(ADVISORY_LOCK_SQL, [key])\n if (rows[0]?.locked !== true) return NOT_ACQUIRED\n try {\n const value = await fn(txEm)\n callback.returned = true\n return { acquired: true, value }\n } catch (error) {\n callback.thrown = true\n callback.error = error\n throw error\n }\n })\n } catch (error) {\n if (callback.thrown) throw callback.error\n if (isTransientLockDbError(error)) {\n throw new AdvisoryLockUnavailableError(callback.returned ? 'commit_unknown' : 'transient_db', key, error)\n }\n throw error\n }\n}\n\n/**\n * Runs `fn` in one transaction that holds `pg_try_advisory_xact_lock(hashtextextended(key, 0))`.\n * - `key` is `<namespace>:<parts>` with namespace `^[a-z][a-z0-9_]*$`; anything else throws `TypeError`.\n * - `waitDeadlineMs` must be a finite number \u2265 0 and `maxConcurrentHolders` a positive integer;\n * anything else throws `TypeError` before any connection is taken.\n * - Pass the request (container) EntityManager. The helper forks it with\n * `{ disableContextResolution: true, clear: true, useContext: false, disableTransactions: false, cloneEventManager: true }`,\n * so the lock transaction never nests in a caller's transaction and commits on its own.\n * - `fn` MUST do all DB I/O on `txEm`, MUST flush its own writes, MUST be bounded (\u2264 one external\n * call), MUST NOT call `transactional` again (it only opens a savepoint in the lock transaction) and\n * MUST NOT fork or use `getConnection().execute` (another pooled connection, outside the lock).\n * - `txEm` carries the request EntityManager's subscribers (cloned event manager).\n * - A failed attempt ends its empty transaction, so waiters hold no connection between attempts.\n * They back off 50 ms doubling up to 800 ms (plus up to 24 ms jitter), never past\n * `waitDeadlineMs` measured from the call start, then retry.\n * - With `maxConcurrentHolders`, each attempt first takes a per-process slot for the key\n * namespace and gives it back when the attempt fails or the lock transaction settles.\n * - Errors from the helper's own begin, lock query and commit that `isTransientLockDbError` accepts,\n * and the deadline, throw `AdvisoryLockUnavailableError` (its `reason` says whether `fn` ran); any\n * other error from them, and every error thrown by `fn`, propagates unchanged (a throw from `fn`\n * rolls back).\n */\nexport async function withAdvisoryXactLock<T>(\n em: EntityManager,\n key: string,\n fn: (txEm: EntityManager) => Promise<T>,\n options: AdvisoryLockOptions<T> = {},\n): Promise<T> {\n const namespace = parseKeyNamespace(key)\n const waitDeadlineMs = resolveWaitDeadlineMs(options.waitDeadlineMs)\n const maxConcurrentHolders = resolveMaxConcurrentHolders(options.maxConcurrentHolders)\n const deadlineAt = Date.now() + waitDeadlineMs\n const lockEm = em.fork(LOCK_FORK_OPTIONS)\n for (let attempt = 1; ; attempt += 1) {\n const releaseSlot = maxConcurrentHolders === null\n ? releaseNothing\n : await acquireSlot(namespace, maxConcurrentHolders, key, deadlineAt)\n let outcome: AttemptOutcome<T>\n try {\n outcome = await attemptLock(lockEm, key, fn)\n } finally {\n releaseSlot()\n }\n if (outcome.acquired) return outcome.value\n const remainingMs = deadlineAt - Date.now()\n if (remainingMs <= 0) throw new AdvisoryLockUnavailableError('deadline', key)\n await sleep(Math.min(calculateBackoffDelayMs(Math.min(attempt, BACKOFF_ATTEMPT_CAP), BACKOFF_OPTIONS), remainingMs))\n if (options.onWait) {\n const waitResult = await options.onWait()\n if (waitResult.done) return waitResult.value\n }\n if (Date.now() >= deadlineAt) throw new AdvisoryLockUnavailableError('deadline', key)\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/dist/lib/db/pg-errors.js
CHANGED
|
@@ -89,10 +89,24 @@ function isTransientDbError(err) {
|
|
|
89
89
|
}
|
|
90
90
|
return false;
|
|
91
91
|
}
|
|
92
|
+
const TRANSIENT_LOCK_SQLSTATES = /* @__PURE__ */ new Set(["55P03", "57014", "25P03"]);
|
|
93
|
+
const TRANSIENT_LOCK_MESSAGE_PATTERNS = [
|
|
94
|
+
/timeout exceeded when trying to connect/i,
|
|
95
|
+
/client has encountered a connection error and is not queryable/i
|
|
96
|
+
];
|
|
97
|
+
function isTransientLockDbError(err) {
|
|
98
|
+
return pgErrorCandidates(err).some((candidate) => {
|
|
99
|
+
if (isTransientDbError(candidate)) return true;
|
|
100
|
+
if (typeof candidate.code === "string" && TRANSIENT_LOCK_SQLSTATES.has(candidate.code)) return true;
|
|
101
|
+
const message = candidate.message;
|
|
102
|
+
return typeof message === "string" && TRANSIENT_LOCK_MESSAGE_PATTERNS.some((pattern) => pattern.test(message));
|
|
103
|
+
});
|
|
104
|
+
}
|
|
92
105
|
export {
|
|
93
106
|
getForeignKeyViolationConstraint,
|
|
94
107
|
isForeignKeyViolation,
|
|
95
108
|
isTransientDbError,
|
|
109
|
+
isTransientLockDbError,
|
|
96
110
|
isUniqueViolation,
|
|
97
111
|
readPgSqlState
|
|
98
112
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/lib/db/pg-errors.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * Detect a Postgres unique-constraint violation (SQLSTATE 23505) regardless of\n * the ORM/driver layer that surfaces it. Shared across modules so duplicate-insert\n * handling stays consistent platform-wide.\n */\nexport function isUniqueViolation(err: unknown): boolean {\n if (!err || typeof err !== 'object') return false\n const code = (err as { code?: string }).code\n if (code === '23505') return true // Postgres unique_violation\n const message = (err as { message?: string }).message\n return typeof message === 'string' && /duplicate key value|unique constraint/i.test(message)\n}\n\nconst FOREIGN_KEY_VIOLATION_MESSAGE = /violates foreign key constraint(?: \"([^\"]+)\")?/i\n\nconst MAX_ERROR_CHAIN_DEPTH = 4\n\n/**\n * MikroORM wraps driver errors and copies the pg fields onto the wrapper, but\n * the original error may also sit behind `cause` (Node) or `previous`\n * (MikroORM), possibly re-wrapped by a transaction helper. Walk that chain,\n * breadth-first with a small depth cap, so a check works on any layer.\n */\nfunction pgErrorCandidates(err: unknown): Array<Record<string, unknown>> {\n const found: Array<Record<string, unknown>> = []\n const seen = new Set<unknown>()\n let layer: unknown[] = [err]\n for (let depth = 0; depth < MAX_ERROR_CHAIN_DEPTH && layer.length > 0; depth += 1) {\n const next: unknown[] = []\n for (const candidate of layer) {\n if (!candidate || typeof candidate !== 'object' || seen.has(candidate)) continue\n seen.add(candidate)\n const record = candidate as Record<string, unknown>\n found.push(record)\n next.push(record.cause, record.previous)\n }\n layer = next\n }\n return found\n}\n\n/**\n * Detect a Postgres foreign-key violation (SQLSTATE 23503): the row is still\n * referenced by a dependent table, or the payload references a parent that\n * does not exist. Looks through MikroORM's driver-error wrapping.\n */\nexport function isForeignKeyViolation(err: unknown): boolean {\n return pgErrorCandidates(err).some((candidate) => {\n if (candidate.code === '23503') return true // Postgres foreign_key_violation\n return typeof candidate.message === 'string' && FOREIGN_KEY_VIOLATION_MESSAGE.test(candidate.message)\n })\n}\n\n/**\n * Name of the constraint behind a foreign-key violation, read from the pg\n * `constraint` field on any layer of the wrapper chain, or parsed out of the\n * quoted constraint in the driver message when the field is missing.\n */\nexport function getForeignKeyViolationConstraint(err: unknown): string | null {\n for (const candidate of pgErrorCandidates(err)) {\n if (typeof candidate.constraint === 'string' && candidate.constraint.length > 0) return candidate.constraint\n }\n for (const candidate of pgErrorCandidates(err)) {\n if (typeof candidate.message !== 'string') continue\n const match = FOREIGN_KEY_VIOLATION_MESSAGE.exec(candidate.message)\n if (match?.[1]) return match[1]\n }\n return null\n}\n\n/**\n * A Postgres SQLSTATE is always exactly 5 characters from `[0-9A-Z]`, and every\n * class in the standard SQLSTATE table carries at least one digit. Node system\n * errors (`ECONNREFUSED`), Node internal errors (`ERR_INVALID_ARG_TYPE`), and the\n * five-letter Node errno codes (`EPIPE`, `EPERM`, `EBUSY`) would otherwise be\n * misread as a SQLSTATE, since they also populate a string `code` field; the\n * digit requirement rules those out without excluding any real SQLSTATE.\n */\nconst SQLSTATE_PATTERN = /^(?=.*[0-9])[0-9A-Z]{5}$/\n\n/**\n * The Postgres SQLSTATE behind an error, read from the pg `code` field on any\n * layer of the driver-error wrapper chain (see `pgErrorCandidates`). Returns\n * `null` when no layer carries a `code` matching the SQLSTATE shape, which\n * also covers non-Postgres errors whose `code` is a Node error code.\n */\nexport function readPgSqlState(err: unknown): string | null {\n for (const candidate of pgErrorCandidates(err)) {\n if (typeof candidate.code === 'string' && SQLSTATE_PATTERN.test(candidate.code)) return candidate.code\n }\n return null\n}\n\n/**\n * Postgres SQLSTATEs for transient connection / availability failures \u2014 the\n * database (or its connection pool) is temporarily unreachable and the request\n * can succeed on retry. Deliberately scoped to connection/availability codes;\n * query-level conflicts (deadlock 40P01, serialization 40001, lock_not_available\n * 55P03) are NOT included because they do not mean \"service unavailable\".\n */\nconst TRANSIENT_CONNECTION_SQLSTATES = new Set([\n '53300', // too_many_connections\n '53400', // configuration_limit_exceeded\n '57P01', // admin_shutdown\n '57P02', // crash_shutdown\n '57P03', // cannot_connect_now (db starting up)\n '08000', // connection_exception\n '08001', // sqlclient_unable_to_establish_sqlconnection\n '08003', // connection_does_not_exist\n '08006', // connection_failure\n])\n\n/**\n * Postgres-driver / connection-pool messages that describe a transient DB\n * connection failure when the SQLSTATE is dropped by the ORM wrapper. These are\n * intentionally DB-specific phrases only. Bare socket codes (ECONNREFUSED,\n * ETIMEDOUT, \u2026) are deliberately NOT matched: they can originate from any\n * outbound socket (HTTP, cache, queue), so keying off them would falsely\n * attribute unrelated failures to the database.\n */\nconst TRANSIENT_DB_MESSAGE_PATTERNS = [\n /too many clients already/i,\n /unable to acquire a connection/i,\n /timeout acquiring a connection/i,\n /connection terminated/i,\n /the database system is (starting up|shutting down|in recovery)/i,\n]\n\n/**\n * Detect a transient Postgres connection / availability failure (pool exhaustion,\n * `max_connections` reached, DB restarting) via its SQLSTATE or a DB-specific\n * driver message. Gates retryable 503 responses so callers do not report a\n * temporary infrastructure blip as an auth failure (401) or an unexpected server\n * error (500). Scoped to unambiguous DB signals so generic socket errors from\n * non-DB calls are never misclassified.\n */\nexport function isTransientDbError(err: unknown): boolean {\n if (!err || typeof err !== 'object') {\n return false\n }\n const code = (err as { code?: string }).code\n if (typeof code === 'string' && TRANSIENT_CONNECTION_SQLSTATES.has(code)) {\n return true\n }\n const message = (err as { message?: string }).message\n if (typeof message === 'string' && TRANSIENT_DB_MESSAGE_PATTERNS.some((pattern) => pattern.test(message))) {\n return true\n }\n return false\n}\n"],
|
|
5
|
-
"mappings": "AAKO,SAAS,kBAAkB,KAAuB;AACvD,MAAI,CAAC,OAAO,OAAO,QAAQ,SAAU,QAAO;AAC5C,QAAM,OAAQ,IAA0B;AACxC,MAAI,SAAS,QAAS,QAAO;AAC7B,QAAM,UAAW,IAA6B;AAC9C,SAAO,OAAO,YAAY,YAAY,yCAAyC,KAAK,OAAO;AAC7F;AAEA,MAAM,gCAAgC;AAEtC,MAAM,wBAAwB;AAQ9B,SAAS,kBAAkB,KAA8C;AACvE,QAAM,QAAwC,CAAC;AAC/C,QAAM,OAAO,oBAAI,IAAa;AAC9B,MAAI,QAAmB,CAAC,GAAG;AAC3B,WAAS,QAAQ,GAAG,QAAQ,yBAAyB,MAAM,SAAS,GAAG,SAAS,GAAG;AACjF,UAAM,OAAkB,CAAC;AACzB,eAAW,aAAa,OAAO;AAC7B,UAAI,CAAC,aAAa,OAAO,cAAc,YAAY,KAAK,IAAI,SAAS,EAAG;AACxE,WAAK,IAAI,SAAS;AAClB,YAAM,SAAS;AACf,YAAM,KAAK,MAAM;AACjB,WAAK,KAAK,OAAO,OAAO,OAAO,QAAQ;AAAA,IACzC;AACA,YAAQ;AAAA,EACV;AACA,SAAO;AACT;AAOO,SAAS,sBAAsB,KAAuB;AAC3D,SAAO,kBAAkB,GAAG,EAAE,KAAK,CAAC,cAAc;AAChD,QAAI,UAAU,SAAS,QAAS,QAAO;AACvC,WAAO,OAAO,UAAU,YAAY,YAAY,8BAA8B,KAAK,UAAU,OAAO;AAAA,EACtG,CAAC;AACH;AAOO,SAAS,iCAAiC,KAA6B;AAC5E,aAAW,aAAa,kBAAkB,GAAG,GAAG;AAC9C,QAAI,OAAO,UAAU,eAAe,YAAY,UAAU,WAAW,SAAS,EAAG,QAAO,UAAU;AAAA,EACpG;AACA,aAAW,aAAa,kBAAkB,GAAG,GAAG;AAC9C,QAAI,OAAO,UAAU,YAAY,SAAU;AAC3C,UAAM,QAAQ,8BAA8B,KAAK,UAAU,OAAO;AAClE,QAAI,QAAQ,CAAC,EAAG,QAAO,MAAM,CAAC;AAAA,EAChC;AACA,SAAO;AACT;AAUA,MAAM,mBAAmB;AAQlB,SAAS,eAAe,KAA6B;AAC1D,aAAW,aAAa,kBAAkB,GAAG,GAAG;AAC9C,QAAI,OAAO,UAAU,SAAS,YAAY,iBAAiB,KAAK,UAAU,IAAI,EAAG,QAAO,UAAU;AAAA,EACpG;AACA,SAAO;AACT;AASA,MAAM,iCAAiC,oBAAI,IAAI;AAAA,EAC7C;AAAA;AAAA,EACA;AAAA;AAAA,EACA;AAAA;AAAA,EACA;AAAA;AAAA,EACA;AAAA;AAAA,EACA;AAAA;AAAA,EACA;AAAA;AAAA,EACA;AAAA;AAAA,EACA;AAAA;AACF,CAAC;AAUD,MAAM,gCAAgC;AAAA,EACpC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAUO,SAAS,mBAAmB,KAAuB;AACxD,MAAI,CAAC,OAAO,OAAO,QAAQ,UAAU;AACnC,WAAO;AAAA,EACT;AACA,QAAM,OAAQ,IAA0B;AACxC,MAAI,OAAO,SAAS,YAAY,+BAA+B,IAAI,IAAI,GAAG;AACxE,WAAO;AAAA,EACT;AACA,QAAM,UAAW,IAA6B;AAC9C,MAAI,OAAO,YAAY,YAAY,8BAA8B,KAAK,CAAC,YAAY,QAAQ,KAAK,OAAO,CAAC,GAAG;AACzG,WAAO;AAAA,EACT;AACA,SAAO;AACT;",
|
|
4
|
+
"sourcesContent": ["/**\n * Detect a Postgres unique-constraint violation (SQLSTATE 23505) regardless of\n * the ORM/driver layer that surfaces it. Shared across modules so duplicate-insert\n * handling stays consistent platform-wide.\n */\nexport function isUniqueViolation(err: unknown): boolean {\n if (!err || typeof err !== 'object') return false\n const code = (err as { code?: string }).code\n if (code === '23505') return true // Postgres unique_violation\n const message = (err as { message?: string }).message\n return typeof message === 'string' && /duplicate key value|unique constraint/i.test(message)\n}\n\nconst FOREIGN_KEY_VIOLATION_MESSAGE = /violates foreign key constraint(?: \"([^\"]+)\")?/i\n\nconst MAX_ERROR_CHAIN_DEPTH = 4\n\n/**\n * MikroORM wraps driver errors and copies the pg fields onto the wrapper, but\n * the original error may also sit behind `cause` (Node) or `previous`\n * (MikroORM), possibly re-wrapped by a transaction helper. Walk that chain,\n * breadth-first with a small depth cap, so a check works on any layer.\n */\nfunction pgErrorCandidates(err: unknown): Array<Record<string, unknown>> {\n const found: Array<Record<string, unknown>> = []\n const seen = new Set<unknown>()\n let layer: unknown[] = [err]\n for (let depth = 0; depth < MAX_ERROR_CHAIN_DEPTH && layer.length > 0; depth += 1) {\n const next: unknown[] = []\n for (const candidate of layer) {\n if (!candidate || typeof candidate !== 'object' || seen.has(candidate)) continue\n seen.add(candidate)\n const record = candidate as Record<string, unknown>\n found.push(record)\n next.push(record.cause, record.previous)\n }\n layer = next\n }\n return found\n}\n\n/**\n * Detect a Postgres foreign-key violation (SQLSTATE 23503): the row is still\n * referenced by a dependent table, or the payload references a parent that\n * does not exist. Looks through MikroORM's driver-error wrapping.\n */\nexport function isForeignKeyViolation(err: unknown): boolean {\n return pgErrorCandidates(err).some((candidate) => {\n if (candidate.code === '23503') return true // Postgres foreign_key_violation\n return typeof candidate.message === 'string' && FOREIGN_KEY_VIOLATION_MESSAGE.test(candidate.message)\n })\n}\n\n/**\n * Name of the constraint behind a foreign-key violation, read from the pg\n * `constraint` field on any layer of the wrapper chain, or parsed out of the\n * quoted constraint in the driver message when the field is missing.\n */\nexport function getForeignKeyViolationConstraint(err: unknown): string | null {\n for (const candidate of pgErrorCandidates(err)) {\n if (typeof candidate.constraint === 'string' && candidate.constraint.length > 0) return candidate.constraint\n }\n for (const candidate of pgErrorCandidates(err)) {\n if (typeof candidate.message !== 'string') continue\n const match = FOREIGN_KEY_VIOLATION_MESSAGE.exec(candidate.message)\n if (match?.[1]) return match[1]\n }\n return null\n}\n\n/**\n * A Postgres SQLSTATE is always exactly 5 characters from `[0-9A-Z]`, and every\n * class in the standard SQLSTATE table carries at least one digit. Node system\n * errors (`ECONNREFUSED`), Node internal errors (`ERR_INVALID_ARG_TYPE`), and the\n * five-letter Node errno codes (`EPIPE`, `EPERM`, `EBUSY`) would otherwise be\n * misread as a SQLSTATE, since they also populate a string `code` field; the\n * digit requirement rules those out without excluding any real SQLSTATE.\n */\nconst SQLSTATE_PATTERN = /^(?=.*[0-9])[0-9A-Z]{5}$/\n\n/**\n * The Postgres SQLSTATE behind an error, read from the pg `code` field on any\n * layer of the driver-error wrapper chain (see `pgErrorCandidates`). Returns\n * `null` when no layer carries a `code` matching the SQLSTATE shape, which\n * also covers non-Postgres errors whose `code` is a Node error code.\n */\nexport function readPgSqlState(err: unknown): string | null {\n for (const candidate of pgErrorCandidates(err)) {\n if (typeof candidate.code === 'string' && SQLSTATE_PATTERN.test(candidate.code)) return candidate.code\n }\n return null\n}\n\n/**\n * Postgres SQLSTATEs for transient connection / availability failures \u2014 the\n * database (or its connection pool) is temporarily unreachable and the request\n * can succeed on retry. Deliberately scoped to connection/availability codes;\n * query-level conflicts (deadlock 40P01, serialization 40001, lock_not_available\n * 55P03) are NOT included because they do not mean \"service unavailable\".\n */\nconst TRANSIENT_CONNECTION_SQLSTATES = new Set([\n '53300', // too_many_connections\n '53400', // configuration_limit_exceeded\n '57P01', // admin_shutdown\n '57P02', // crash_shutdown\n '57P03', // cannot_connect_now (db starting up)\n '08000', // connection_exception\n '08001', // sqlclient_unable_to_establish_sqlconnection\n '08003', // connection_does_not_exist\n '08006', // connection_failure\n])\n\n/**\n * Postgres-driver / connection-pool messages that describe a transient DB\n * connection failure when the SQLSTATE is dropped by the ORM wrapper. These are\n * intentionally DB-specific phrases only. Bare socket codes (ECONNREFUSED,\n * ETIMEDOUT, \u2026) are deliberately NOT matched: they can originate from any\n * outbound socket (HTTP, cache, queue), so keying off them would falsely\n * attribute unrelated failures to the database.\n */\nconst TRANSIENT_DB_MESSAGE_PATTERNS = [\n /too many clients already/i,\n /unable to acquire a connection/i,\n /timeout acquiring a connection/i,\n /connection terminated/i,\n /the database system is (starting up|shutting down|in recovery)/i,\n]\n\n/**\n * Detect a transient Postgres connection / availability failure (pool exhaustion,\n * `max_connections` reached, DB restarting) via its SQLSTATE or a DB-specific\n * driver message. Gates retryable 503 responses so callers do not report a\n * temporary infrastructure blip as an auth failure (401) or an unexpected server\n * error (500). Scoped to unambiguous DB signals so generic socket errors from\n * non-DB calls are never misclassified.\n */\nexport function isTransientDbError(err: unknown): boolean {\n if (!err || typeof err !== 'object') {\n return false\n }\n const code = (err as { code?: string }).code\n if (typeof code === 'string' && TRANSIENT_CONNECTION_SQLSTATES.has(code)) {\n return true\n }\n const message = (err as { message?: string }).message\n if (typeof message === 'string' && TRANSIENT_DB_MESSAGE_PATTERNS.some((pattern) => pattern.test(message))) {\n return true\n }\n return false\n}\n\n/**\n * SQLSTATEs that end a lock transaction early but say nothing about the data it\n * would have written, so the work can be retried: lock_not_available (55P03,\n * raised by `DB_LOCK_TIMEOUT_MS`), query_canceled (57014, raised by the opt-in\n * `DB_STATEMENT_TIMEOUT_MS`) and idle_in_transaction_session_timeout (25P03).\n */\nconst TRANSIENT_LOCK_SQLSTATES = new Set(['55P03', '57014', '25P03'])\n\n/**\n * Driver messages for a connection that never arrived or was lost mid-transaction:\n * the pg-pool acquire timeout (`connectionTimeoutMillis`) and node-postgres's\n * error for a client whose connection already failed.\n */\nconst TRANSIENT_LOCK_MESSAGE_PATTERNS = [\n /timeout exceeded when trying to connect/i,\n /client has encountered a connection error and is not queryable/i,\n]\n\n/**\n * @internal Detect a transient failure of a short lock transaction: everything\n * `isTransientDbError` matches, plus lock, statement and idle-in-transaction\n * timeouts, the pg-pool acquire timeout and node-postgres's \"not queryable\"\n * error. Checks every layer of the driver-error wrapper chain (`cause` /\n * `previous`, see `pgErrorCandidates`). `isTransientDbError` keeps its narrower\n * scope because its callers answer 503 \"service unavailable\".\n */\nexport function isTransientLockDbError(err: unknown): boolean {\n return pgErrorCandidates(err).some((candidate) => {\n if (isTransientDbError(candidate)) return true\n if (typeof candidate.code === 'string' && TRANSIENT_LOCK_SQLSTATES.has(candidate.code)) return true\n const message = candidate.message\n return typeof message === 'string' && TRANSIENT_LOCK_MESSAGE_PATTERNS.some((pattern) => pattern.test(message))\n })\n}\n"],
|
|
5
|
+
"mappings": "AAKO,SAAS,kBAAkB,KAAuB;AACvD,MAAI,CAAC,OAAO,OAAO,QAAQ,SAAU,QAAO;AAC5C,QAAM,OAAQ,IAA0B;AACxC,MAAI,SAAS,QAAS,QAAO;AAC7B,QAAM,UAAW,IAA6B;AAC9C,SAAO,OAAO,YAAY,YAAY,yCAAyC,KAAK,OAAO;AAC7F;AAEA,MAAM,gCAAgC;AAEtC,MAAM,wBAAwB;AAQ9B,SAAS,kBAAkB,KAA8C;AACvE,QAAM,QAAwC,CAAC;AAC/C,QAAM,OAAO,oBAAI,IAAa;AAC9B,MAAI,QAAmB,CAAC,GAAG;AAC3B,WAAS,QAAQ,GAAG,QAAQ,yBAAyB,MAAM,SAAS,GAAG,SAAS,GAAG;AACjF,UAAM,OAAkB,CAAC;AACzB,eAAW,aAAa,OAAO;AAC7B,UAAI,CAAC,aAAa,OAAO,cAAc,YAAY,KAAK,IAAI,SAAS,EAAG;AACxE,WAAK,IAAI,SAAS;AAClB,YAAM,SAAS;AACf,YAAM,KAAK,MAAM;AACjB,WAAK,KAAK,OAAO,OAAO,OAAO,QAAQ;AAAA,IACzC;AACA,YAAQ;AAAA,EACV;AACA,SAAO;AACT;AAOO,SAAS,sBAAsB,KAAuB;AAC3D,SAAO,kBAAkB,GAAG,EAAE,KAAK,CAAC,cAAc;AAChD,QAAI,UAAU,SAAS,QAAS,QAAO;AACvC,WAAO,OAAO,UAAU,YAAY,YAAY,8BAA8B,KAAK,UAAU,OAAO;AAAA,EACtG,CAAC;AACH;AAOO,SAAS,iCAAiC,KAA6B;AAC5E,aAAW,aAAa,kBAAkB,GAAG,GAAG;AAC9C,QAAI,OAAO,UAAU,eAAe,YAAY,UAAU,WAAW,SAAS,EAAG,QAAO,UAAU;AAAA,EACpG;AACA,aAAW,aAAa,kBAAkB,GAAG,GAAG;AAC9C,QAAI,OAAO,UAAU,YAAY,SAAU;AAC3C,UAAM,QAAQ,8BAA8B,KAAK,UAAU,OAAO;AAClE,QAAI,QAAQ,CAAC,EAAG,QAAO,MAAM,CAAC;AAAA,EAChC;AACA,SAAO;AACT;AAUA,MAAM,mBAAmB;AAQlB,SAAS,eAAe,KAA6B;AAC1D,aAAW,aAAa,kBAAkB,GAAG,GAAG;AAC9C,QAAI,OAAO,UAAU,SAAS,YAAY,iBAAiB,KAAK,UAAU,IAAI,EAAG,QAAO,UAAU;AAAA,EACpG;AACA,SAAO;AACT;AASA,MAAM,iCAAiC,oBAAI,IAAI;AAAA,EAC7C;AAAA;AAAA,EACA;AAAA;AAAA,EACA;AAAA;AAAA,EACA;AAAA;AAAA,EACA;AAAA;AAAA,EACA;AAAA;AAAA,EACA;AAAA;AAAA,EACA;AAAA;AAAA,EACA;AAAA;AACF,CAAC;AAUD,MAAM,gCAAgC;AAAA,EACpC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAUO,SAAS,mBAAmB,KAAuB;AACxD,MAAI,CAAC,OAAO,OAAO,QAAQ,UAAU;AACnC,WAAO;AAAA,EACT;AACA,QAAM,OAAQ,IAA0B;AACxC,MAAI,OAAO,SAAS,YAAY,+BAA+B,IAAI,IAAI,GAAG;AACxE,WAAO;AAAA,EACT;AACA,QAAM,UAAW,IAA6B;AAC9C,MAAI,OAAO,YAAY,YAAY,8BAA8B,KAAK,CAAC,YAAY,QAAQ,KAAK,OAAO,CAAC,GAAG;AACzG,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAQA,MAAM,2BAA2B,oBAAI,IAAI,CAAC,SAAS,SAAS,OAAO,CAAC;AAOpE,MAAM,kCAAkC;AAAA,EACtC;AAAA,EACA;AACF;AAUO,SAAS,uBAAuB,KAAuB;AAC5D,SAAO,kBAAkB,GAAG,EAAE,KAAK,CAAC,cAAc;AAChD,QAAI,mBAAmB,SAAS,EAAG,QAAO;AAC1C,QAAI,OAAO,UAAU,SAAS,YAAY,yBAAyB,IAAI,UAAU,IAAI,EAAG,QAAO;AAC/F,UAAM,UAAU,UAAU;AAC1B,WAAO,OAAO,YAAY,YAAY,gCAAgC,KAAK,CAAC,YAAY,QAAQ,KAAK,OAAO,CAAC;AAAA,EAC/G,CAAC;AACH;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
import { applyLocalizedContent } from "./resolver.js";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
registerTranslatableFields,
|
|
4
|
+
getTranslatableFields,
|
|
5
|
+
getTranslatableFieldsRegistry,
|
|
6
|
+
registerTranslatableFieldExpander,
|
|
7
|
+
getTranslatableFieldExpander
|
|
8
|
+
} from "./translatable-fields.js";
|
|
3
9
|
import { registerTranslationOverlayPlugin, getTranslationOverlayPlugin } from "./overlay-plugin.js";
|
|
4
10
|
export {
|
|
5
11
|
applyLocalizedContent,
|
|
12
|
+
getTranslatableFieldExpander,
|
|
6
13
|
getTranslatableFields,
|
|
7
14
|
getTranslatableFieldsRegistry,
|
|
8
15
|
getTranslationOverlayPlugin,
|
|
16
|
+
registerTranslatableFieldExpander,
|
|
9
17
|
registerTranslatableFields,
|
|
10
18
|
registerTranslationOverlayPlugin
|
|
11
19
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/lib/localization/index.ts"],
|
|
4
|
-
"sourcesContent": ["export type { TranslationRecord, LocaleCode } from './types'\nexport { applyLocalizedContent } from './resolver'\nexport {
|
|
5
|
-
"mappings": "AACA,SAAS,6BAA6B;AACtC,
|
|
4
|
+
"sourcesContent": ["export type { TranslationRecord, LocaleCode } from './types'\nexport { applyLocalizedContent } from './resolver'\nexport {\n registerTranslatableFields,\n getTranslatableFields,\n getTranslatableFieldsRegistry,\n registerTranslatableFieldExpander,\n getTranslatableFieldExpander,\n} from './translatable-fields'\nexport type { TranslatableFieldExpansion, TranslatableFieldExpander } from './translatable-fields'\nexport { registerTranslationOverlayPlugin, getTranslationOverlayPlugin } from './overlay-plugin'\nexport type { TranslationOverlayFn, ResolveLocaleFromRequestFn } from './overlay-plugin'\n"],
|
|
5
|
+
"mappings": "AACA,SAAS,6BAA6B;AACtC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEP,SAAS,kCAAkC,mCAAmC;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -14,9 +14,35 @@ function getTranslatableFields(entityType) {
|
|
|
14
14
|
function getTranslatableFieldsRegistry() {
|
|
15
15
|
return { ...getGlobal() };
|
|
16
16
|
}
|
|
17
|
+
const EXPANDER_GLOBAL_KEY = "__openMercatoTranslatableFieldExpanders__";
|
|
18
|
+
function getExpanderGlobal() {
|
|
19
|
+
return globalThis[EXPANDER_GLOBAL_KEY] ?? {};
|
|
20
|
+
}
|
|
21
|
+
function registerTranslatableFieldExpander(entityType, expander) {
|
|
22
|
+
const host = globalThis;
|
|
23
|
+
host[EXPANDER_GLOBAL_KEY] = { ...getExpanderGlobal(), [entityType]: expander };
|
|
24
|
+
}
|
|
25
|
+
function getTranslatableFieldExpander(entityType) {
|
|
26
|
+
return getExpanderGlobal()[entityType];
|
|
27
|
+
}
|
|
28
|
+
const LIST_PATH_GLOBAL_KEY = "__openMercatoTranslatableEntityListPaths__";
|
|
29
|
+
function getListPathGlobal() {
|
|
30
|
+
return globalThis[LIST_PATH_GLOBAL_KEY] ?? {};
|
|
31
|
+
}
|
|
32
|
+
function registerTranslatableEntityListPath(entityType, listPath) {
|
|
33
|
+
const host = globalThis;
|
|
34
|
+
host[LIST_PATH_GLOBAL_KEY] = { ...getListPathGlobal(), [entityType]: listPath };
|
|
35
|
+
}
|
|
36
|
+
function getTranslatableEntityListPath(entityType) {
|
|
37
|
+
return getListPathGlobal()[entityType];
|
|
38
|
+
}
|
|
17
39
|
export {
|
|
40
|
+
getTranslatableEntityListPath,
|
|
41
|
+
getTranslatableFieldExpander,
|
|
18
42
|
getTranslatableFields,
|
|
19
43
|
getTranslatableFieldsRegistry,
|
|
44
|
+
registerTranslatableEntityListPath,
|
|
45
|
+
registerTranslatableFieldExpander,
|
|
20
46
|
registerTranslatableFields
|
|
21
47
|
};
|
|
22
48
|
//# sourceMappingURL=translatable-fields.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/lib/localization/translatable-fields.ts"],
|
|
4
|
-
"sourcesContent": ["type TranslatableFieldsRegistry = Record<string, string[]>\n\n// Use globalThis to survive Turbopack/esbuild module duplication where the same\n// file can be loaded as multiple module instances when mixing dynamic and static imports\nconst GLOBAL_KEY = '__openMercatoTranslatableFields__'\n\nfunction getGlobal(): TranslatableFieldsRegistry {\n return (globalThis as any)[GLOBAL_KEY] ?? {}\n}\n\nfunction setGlobal(registry: TranslatableFieldsRegistry): void {\n (globalThis as any)[GLOBAL_KEY] = registry\n}\n\nexport function registerTranslatableFields(fields: TranslatableFieldsRegistry): void {\n setGlobal({ ...getGlobal(), ...fields })\n}\n\nexport function getTranslatableFields(entityType: string): string[] | undefined {\n return getGlobal()[entityType]\n}\n\nexport function getTranslatableFieldsRegistry(): TranslatableFieldsRegistry {\n return { ...getGlobal() }\n}\n"],
|
|
5
|
-
"mappings": "AAIA,MAAM,aAAa;AAEnB,SAAS,YAAwC;AAC/C,SAAQ,WAAmB,UAAU,KAAK,CAAC;AAC7C;AAEA,SAAS,UAAU,UAA4C;AAC7D,EAAC,WAAmB,UAAU,IAAI;AACpC;AAEO,SAAS,2BAA2B,QAA0C;AACnF,YAAU,EAAE,GAAG,UAAU,GAAG,GAAG,OAAO,CAAC;AACzC;AAEO,SAAS,sBAAsB,YAA0C;AAC9E,SAAO,UAAU,EAAE,UAAU;AAC/B;AAEO,SAAS,gCAA4D;AAC1E,SAAO,EAAE,GAAG,UAAU,EAAE;AAC1B;",
|
|
4
|
+
"sourcesContent": ["type TranslatableFieldsRegistry = Record<string, string[]>\n\n// Use globalThis to survive Turbopack/esbuild module duplication where the same\n// file can be loaded as multiple module instances when mixing dynamic and static imports\nconst GLOBAL_KEY = '__openMercatoTranslatableFields__'\n\nfunction getGlobal(): TranslatableFieldsRegistry {\n return (globalThis as any)[GLOBAL_KEY] ?? {}\n}\n\nfunction setGlobal(registry: TranslatableFieldsRegistry): void {\n (globalThis as any)[GLOBAL_KEY] = registry\n}\n\nexport function registerTranslatableFields(fields: TranslatableFieldsRegistry): void {\n setGlobal({ ...getGlobal(), ...fields })\n}\n\nexport function getTranslatableFields(entityType: string): string[] | undefined {\n return getGlobal()[entityType]\n}\n\nexport function getTranslatableFieldsRegistry(): TranslatableFieldsRegistry {\n return { ...getGlobal() }\n}\n\nexport type TranslatableFieldExpansion = {\n key: string\n label?: string\n baseValue?: string\n}\n\nexport type TranslatableFieldExpander = (record: Record<string, unknown>) => TranslatableFieldExpansion[]\n\ntype TranslatableFieldExpanderRegistry = Record<string, TranslatableFieldExpander>\n\nconst EXPANDER_GLOBAL_KEY = '__openMercatoTranslatableFieldExpanders__'\n\ntype ExpanderGlobalHost = { [EXPANDER_GLOBAL_KEY]?: TranslatableFieldExpanderRegistry }\n\nfunction getExpanderGlobal(): TranslatableFieldExpanderRegistry {\n return (globalThis as ExpanderGlobalHost)[EXPANDER_GLOBAL_KEY] ?? {}\n}\n\nexport function registerTranslatableFieldExpander(entityType: string, expander: TranslatableFieldExpander): void {\n const host = globalThis as ExpanderGlobalHost\n host[EXPANDER_GLOBAL_KEY] = { ...getExpanderGlobal(), [entityType]: expander }\n}\n\nexport function getTranslatableFieldExpander(entityType: string): TranslatableFieldExpander | undefined {\n return getExpanderGlobal()[entityType]\n}\n\ntype TranslatableEntityListPathRegistry = Record<string, string>\n\nconst LIST_PATH_GLOBAL_KEY = '__openMercatoTranslatableEntityListPaths__'\n\ntype ListPathGlobalHost = { [LIST_PATH_GLOBAL_KEY]?: TranslatableEntityListPathRegistry }\n\nfunction getListPathGlobal(): TranslatableEntityListPathRegistry {\n return (globalThis as ListPathGlobalHost)[LIST_PATH_GLOBAL_KEY] ?? {}\n}\n\nexport function registerTranslatableEntityListPath(entityType: string, listPath: string): void {\n const host = globalThis as ListPathGlobalHost\n host[LIST_PATH_GLOBAL_KEY] = { ...getListPathGlobal(), [entityType]: listPath }\n}\n\nexport function getTranslatableEntityListPath(entityType: string): string | undefined {\n return getListPathGlobal()[entityType]\n}\n"],
|
|
5
|
+
"mappings": "AAIA,MAAM,aAAa;AAEnB,SAAS,YAAwC;AAC/C,SAAQ,WAAmB,UAAU,KAAK,CAAC;AAC7C;AAEA,SAAS,UAAU,UAA4C;AAC7D,EAAC,WAAmB,UAAU,IAAI;AACpC;AAEO,SAAS,2BAA2B,QAA0C;AACnF,YAAU,EAAE,GAAG,UAAU,GAAG,GAAG,OAAO,CAAC;AACzC;AAEO,SAAS,sBAAsB,YAA0C;AAC9E,SAAO,UAAU,EAAE,UAAU;AAC/B;AAEO,SAAS,gCAA4D;AAC1E,SAAO,EAAE,GAAG,UAAU,EAAE;AAC1B;AAYA,MAAM,sBAAsB;AAI5B,SAAS,oBAAuD;AAC9D,SAAQ,WAAkC,mBAAmB,KAAK,CAAC;AACrE;AAEO,SAAS,kCAAkC,YAAoB,UAA2C;AAC/G,QAAM,OAAO;AACb,OAAK,mBAAmB,IAAI,EAAE,GAAG,kBAAkB,GAAG,CAAC,UAAU,GAAG,SAAS;AAC/E;AAEO,SAAS,6BAA6B,YAA2D;AACtG,SAAO,kBAAkB,EAAE,UAAU;AACvC;AAIA,MAAM,uBAAuB;AAI7B,SAAS,oBAAwD;AAC/D,SAAQ,WAAkC,oBAAoB,KAAK,CAAC;AACtE;AAEO,SAAS,mCAAmC,YAAoB,UAAwB;AAC7F,QAAM,OAAO;AACb,OAAK,oBAAoB,IAAI,EAAE,GAAG,kBAAkB,GAAG,CAAC,UAAU,GAAG,SAAS;AAChF;AAEO,SAAS,8BAA8B,YAAwC;AACpF,SAAO,kBAAkB,EAAE,UAAU;AACvC;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/lib/number.js
CHANGED
|
@@ -62,6 +62,11 @@ function parseLocaleNumber(input, locale) {
|
|
|
62
62
|
const parsed = Number(normalized);
|
|
63
63
|
return Number.isFinite(parsed) ? parsed : null;
|
|
64
64
|
}
|
|
65
|
+
function formatLocaleNumberInput(value, locale) {
|
|
66
|
+
const serialized = String(value);
|
|
67
|
+
const { decimal } = resolveLocaleNumberSeparators(locale);
|
|
68
|
+
return decimal === "." ? serialized : serialized.replace(".", decimal);
|
|
69
|
+
}
|
|
65
70
|
function parseNumberWithDefault(raw, fallback, options) {
|
|
66
71
|
if (raw == null) return fallback;
|
|
67
72
|
const trimmed = raw.trim();
|
|
@@ -73,6 +78,7 @@ function parseNumberWithDefault(raw, fallback, options) {
|
|
|
73
78
|
return value;
|
|
74
79
|
}
|
|
75
80
|
export {
|
|
81
|
+
formatLocaleNumberInput,
|
|
76
82
|
parseLocaleNumber,
|
|
77
83
|
parseNumberWithDefault,
|
|
78
84
|
resolveLocaleNumberSeparators
|
package/dist/lib/number.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/lib/number.ts"],
|
|
4
|
-
"sourcesContent": ["type LocaleNumberSeparators = { group: string; decimal: string }\n\nconst DEFAULT_SEPARATORS: LocaleNumberSeparators = { group: ',', decimal: '.' }\nconst separatorCache = new Map<string, LocaleNumberSeparators>()\n\nconst UNICODE_MINUS_SIGNS = /[\u2212\u2012\u2013\u2014]/g\nconst GROUP_LIKE_CHARACTER = /[\\s'\u2019\u02BC]/\nconst GROUP_LIKE_SEPARATOR_IN_POSITION = /(\\d)[\\s'\u2019\u02BC](?=\\d{3}(?!\\d))/g\nconst NORMALIZED_NUMBER = /^[+-]?(\\d+(\\.\\d*)?|\\.\\d+)(e[+-]?\\d+)?$/i\n\n/**\n * Group and decimal separators the given locale uses, derived from `Intl` rather than\n * assumed, so grouping characters such as the narrow no-break space (`fr-FR`) are covered.\n */\nexport function resolveLocaleNumberSeparators(locale?: string): LocaleNumberSeparators {\n const cacheKey = locale ?? ''\n const cached = separatorCache.get(cacheKey)\n if (cached) return cached\n let resolved = DEFAULT_SEPARATORS\n try {\n const parts = new Intl.NumberFormat(locale, {\n useGrouping: true,\n minimumFractionDigits: 1,\n }).formatToParts(12345.6)\n const group = parts.find((part) => part.type === 'group')?.value ?? DEFAULT_SEPARATORS.group\n const decimal = parts.find((part) => part.type === 'decimal')?.value ?? DEFAULT_SEPARATORS.decimal\n resolved = { group, decimal }\n } catch {\n resolved = DEFAULT_SEPARATORS\n }\n separatorCache.set(cacheKey, resolved)\n return resolved\n}\n\nfunction isValidGrouping(integerPart: string, separator: string): boolean {\n const digits = integerPart.replace(/^[+-]/, '')\n const segments = digits.split(separator)\n if (segments.length < 2) return true\n const [first, ...rest] = segments\n if (!/^\\d{1,3}$/.test(first)) return false\n return rest.every((segment) => /^\\d{3}$/.test(segment))\n}\n\n/**\n * Parses a user-typed number written in the conventions of `locale` \u2014 `110,70` under `pl-PL`,\n * `1 234,56` under `fr-FR`, `1,234.56` under `en-US`. Returns `null` when the input is not a\n * number, never a silent `0`, so callers can tell \"unparseable\" apart from \"zero\".\n *\n * Both `,` and `.` are accepted as the decimal separator whichever way the locale runs, because\n * users type the shape their keyboard offers. A SINGLE `,` or `.` is therefore always the decimal\n * point, in every locale: `1.500` is 1.5 under `de-DE` just as it is under `en-US`. Reading a lone\n * separator as grouping instead would turn `1.500` into 1500 with no visible cue \u2014 a silent 1000\u00D7\n * on a money field, and three- and four-decimal unit prices are ordinary here. Grouping is\n * recognized only where it is unambiguous: at least two separators (`1.234.567`), or a whitespace\n * or apostrophe separator standing in a valid group-of-three position (`1 234,56`, `1\u2019234.5`).\n * Whitespace and apostrophes anywhere else are not absorbed \u2014 `1 2` is rejected rather than read\n * as 12 \u2014 so a mistyped or pasted value surfaces as an error instead of a different number.\n *\n * Use it only on strings a user typed. Values arriving from an API or the database are already\n * numbers and MUST NOT go through it.\n */\nexport function parseLocaleNumber(input: string | null | undefined, locale?: string): number | null {\n if (input == null) return null\n const trimmed = input.trim()\n if (!trimmed) return null\n\n const { group } = resolveLocaleNumberSeparators(locale)\n let candidate = trimmed.replace(UNICODE_MINUS_SIGNS, '-')\n if (group && group !== ',' && group !== '.' && !GROUP_LIKE_CHARACTER.test(group)) {\n candidate = candidate.split(group).join(' ')\n }\n candidate = candidate.replace(GROUP_LIKE_SEPARATOR_IN_POSITION, '$1')\n if (!candidate) return null\n\n const hasComma = candidate.includes(',')\n const hasDot = candidate.includes('.')\n let decimalSeparator: string | null = null\n if (hasComma && hasDot) {\n decimalSeparator = candidate.lastIndexOf(',') > candidate.lastIndexOf('.') ? ',' : '.'\n } else if (hasComma || hasDot) {\n const separator = hasComma ? ',' : '.'\n const segments = candidate.split(separator)\n decimalSeparator = segments.length > 2 ? null : separator\n }\n\n const groupSeparator = decimalSeparator\n ? decimalSeparator === ','\n ? '.'\n : ','\n : hasComma\n ? ','\n : '.'\n const [integerPart, ...fractionParts] = decimalSeparator ? candidate.split(decimalSeparator) : [candidate]\n if (fractionParts.length > 1) return null\n if (!isValidGrouping(integerPart, groupSeparator)) return null\n if (fractionParts.length && fractionParts[0].includes(groupSeparator)) return null\n\n const normalized = `${integerPart.split(groupSeparator).join('')}${fractionParts.length ? `.${fractionParts[0]}` : ''}`\n if (!NORMALIZED_NUMBER.test(normalized)) return null\n const parsed = Number(normalized)\n return Number.isFinite(parsed) ? parsed : null\n}\n\nexport function parseNumberWithDefault(\n raw: string | null | undefined,\n fallback: number,\n options?: { min?: number; integer?: boolean },\n): number {\n if (raw == null) return fallback\n const trimmed = raw.trim()\n if (!trimmed) return fallback\n const value = options?.integer ? Number.parseInt(trimmed, 10) : Number(trimmed)\n if (!Number.isFinite(value)) return fallback\n const min = options?.min ?? -Infinity\n if (value < min) return fallback\n return value\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["type LocaleNumberSeparators = { group: string; decimal: string }\n\nconst DEFAULT_SEPARATORS: LocaleNumberSeparators = { group: ',', decimal: '.' }\nconst separatorCache = new Map<string, LocaleNumberSeparators>()\n\nconst UNICODE_MINUS_SIGNS = /[\u2212\u2012\u2013\u2014]/g\nconst GROUP_LIKE_CHARACTER = /[\\s'\u2019\u02BC]/\nconst GROUP_LIKE_SEPARATOR_IN_POSITION = /(\\d)[\\s'\u2019\u02BC](?=\\d{3}(?!\\d))/g\nconst NORMALIZED_NUMBER = /^[+-]?(\\d+(\\.\\d*)?|\\.\\d+)(e[+-]?\\d+)?$/i\n\n/**\n * Group and decimal separators the given locale uses, derived from `Intl` rather than\n * assumed, so grouping characters such as the narrow no-break space (`fr-FR`) are covered.\n */\nexport function resolveLocaleNumberSeparators(locale?: string): LocaleNumberSeparators {\n const cacheKey = locale ?? ''\n const cached = separatorCache.get(cacheKey)\n if (cached) return cached\n let resolved = DEFAULT_SEPARATORS\n try {\n const parts = new Intl.NumberFormat(locale, {\n useGrouping: true,\n minimumFractionDigits: 1,\n }).formatToParts(12345.6)\n const group = parts.find((part) => part.type === 'group')?.value ?? DEFAULT_SEPARATORS.group\n const decimal = parts.find((part) => part.type === 'decimal')?.value ?? DEFAULT_SEPARATORS.decimal\n resolved = { group, decimal }\n } catch {\n resolved = DEFAULT_SEPARATORS\n }\n separatorCache.set(cacheKey, resolved)\n return resolved\n}\n\nfunction isValidGrouping(integerPart: string, separator: string): boolean {\n const digits = integerPart.replace(/^[+-]/, '')\n const segments = digits.split(separator)\n if (segments.length < 2) return true\n const [first, ...rest] = segments\n if (!/^\\d{1,3}$/.test(first)) return false\n return rest.every((segment) => /^\\d{3}$/.test(segment))\n}\n\n/**\n * Parses a user-typed number written in the conventions of `locale` \u2014 `110,70` under `pl-PL`,\n * `1 234,56` under `fr-FR`, `1,234.56` under `en-US`. Returns `null` when the input is not a\n * number, never a silent `0`, so callers can tell \"unparseable\" apart from \"zero\".\n *\n * Both `,` and `.` are accepted as the decimal separator whichever way the locale runs, because\n * users type the shape their keyboard offers. A SINGLE `,` or `.` is therefore always the decimal\n * point, in every locale: `1.500` is 1.5 under `de-DE` just as it is under `en-US`. Reading a lone\n * separator as grouping instead would turn `1.500` into 1500 with no visible cue \u2014 a silent 1000\u00D7\n * on a money field, and three- and four-decimal unit prices are ordinary here. Grouping is\n * recognized only where it is unambiguous: at least two separators (`1.234.567`), or a whitespace\n * or apostrophe separator standing in a valid group-of-three position (`1 234,56`, `1\u2019234.5`).\n * Whitespace and apostrophes anywhere else are not absorbed \u2014 `1 2` is rejected rather than read\n * as 12 \u2014 so a mistyped or pasted value surfaces as an error instead of a different number.\n *\n * Use it only on strings a user typed. Values arriving from an API or the database are already\n * numbers and MUST NOT go through it.\n */\nexport function parseLocaleNumber(input: string | null | undefined, locale?: string): number | null {\n if (input == null) return null\n const trimmed = input.trim()\n if (!trimmed) return null\n\n const { group } = resolveLocaleNumberSeparators(locale)\n let candidate = trimmed.replace(UNICODE_MINUS_SIGNS, '-')\n if (group && group !== ',' && group !== '.' && !GROUP_LIKE_CHARACTER.test(group)) {\n candidate = candidate.split(group).join(' ')\n }\n candidate = candidate.replace(GROUP_LIKE_SEPARATOR_IN_POSITION, '$1')\n if (!candidate) return null\n\n const hasComma = candidate.includes(',')\n const hasDot = candidate.includes('.')\n let decimalSeparator: string | null = null\n if (hasComma && hasDot) {\n decimalSeparator = candidate.lastIndexOf(',') > candidate.lastIndexOf('.') ? ',' : '.'\n } else if (hasComma || hasDot) {\n const separator = hasComma ? ',' : '.'\n const segments = candidate.split(separator)\n decimalSeparator = segments.length > 2 ? null : separator\n }\n\n const groupSeparator = decimalSeparator\n ? decimalSeparator === ','\n ? '.'\n : ','\n : hasComma\n ? ','\n : '.'\n const [integerPart, ...fractionParts] = decimalSeparator ? candidate.split(decimalSeparator) : [candidate]\n if (fractionParts.length > 1) return null\n if (!isValidGrouping(integerPart, groupSeparator)) return null\n if (fractionParts.length && fractionParts[0].includes(groupSeparator)) return null\n\n const normalized = `${integerPart.split(groupSeparator).join('')}${fractionParts.length ? `.${fractionParts[0]}` : ''}`\n if (!NORMALIZED_NUMBER.test(normalized)) return null\n const parsed = Number(normalized)\n return Number.isFinite(parsed) ? parsed : null\n}\n\n/**\n * Renders a number as the text an input field should show under `locale` \u2014 `2,5` under\n * `pl-PL`, `2.5` under `en-US`. Unlike `String()`, which always emits a dot, the result uses\n * the locale's decimal separator, so a value redisplayed after blur or reload matches what the\n * user typed and what the rest of the UI shows. No grouping is applied, and the output always\n * round-trips through `parseLocaleNumber`.\n */\nexport function formatLocaleNumberInput(value: number, locale?: string): string {\n const serialized = String(value)\n const { decimal } = resolveLocaleNumberSeparators(locale)\n return decimal === '.' ? serialized : serialized.replace('.', decimal)\n}\n\nexport function parseNumberWithDefault(\n raw: string | null | undefined,\n fallback: number,\n options?: { min?: number; integer?: boolean },\n): number {\n if (raw == null) return fallback\n const trimmed = raw.trim()\n if (!trimmed) return fallback\n const value = options?.integer ? Number.parseInt(trimmed, 10) : Number(trimmed)\n if (!Number.isFinite(value)) return fallback\n const min = options?.min ?? -Infinity\n if (value < min) return fallback\n return value\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|