@objectstack/core 17.1.0 → 17.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +196 -0
- package/dist/index.cjs +170 -77
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +50 -16
- package/dist/index.d.ts +50 -16
- package/dist/index.js +170 -77
- package/dist/index.js.map +1 -1
- package/package.json +4 -3
package/dist/index.d.cts
CHANGED
|
@@ -320,12 +320,14 @@ declare class ObjectKernel {
|
|
|
320
320
|
* as well: if the hook never settles and nothing else keeps the loop alive,
|
|
321
321
|
* Node exits before the timer can fire and the timeout is never reported.
|
|
322
322
|
* The guard has to stay ref'd exactly as long as the race is undecided,
|
|
323
|
-
* which is what
|
|
323
|
+
* which is what clearing on settle expresses.
|
|
324
324
|
*
|
|
325
|
-
*
|
|
326
|
-
*
|
|
327
|
-
*
|
|
328
|
-
*
|
|
325
|
+
* Clearing the timer was only half of it, though (#10604): the promise the
|
|
326
|
+
* race still holds a reaction on has to SETTLE, or it and that reaction are
|
|
327
|
+
* retained past the end of the run — two leaking promises per boot, which
|
|
328
|
+
* is what `vitest --detectAsyncLeaks` names here. Both halves now live in
|
|
329
|
+
* `TimeoutGuard.reclaim()`, shared with `shutdown()`, so the two sites
|
|
330
|
+
* cannot drift into doing one half each again.
|
|
329
331
|
*/
|
|
330
332
|
private raceStartupTimeout;
|
|
331
333
|
/**
|
|
@@ -2021,17 +2023,27 @@ interface ResolveLocalizationInput {
|
|
|
2021
2023
|
tenantId?: string;
|
|
2022
2024
|
userId?: string;
|
|
2023
2025
|
}
|
|
2026
|
+
type LocalizationResult = {
|
|
2027
|
+
timezone: string;
|
|
2028
|
+
locale: string;
|
|
2029
|
+
currency?: string;
|
|
2030
|
+
};
|
|
2024
2031
|
/**
|
|
2025
2032
|
* Resolve workspace localization defaults (reference `timezone` / `locale` /
|
|
2026
2033
|
* `currency`). Canonical path is the `localization` SettingsManifest (cascade:
|
|
2027
2034
|
* platform default → global → tenant); falls back to direct tenant-scoped
|
|
2028
2035
|
* `sys_setting` rows, then the built-ins `UTC` / `en-US`. Never throws.
|
|
2036
|
+
*
|
|
2037
|
+
* A read that fails outright (backend fault — table missing, connection
|
|
2038
|
+
* refused, etc.) is memoized for {@link LOCALIZATION_FAILURE_CACHE_TTL_MS}
|
|
2039
|
+
* per `(ql, tenantId, userId)` so the failing query — and the driver's log
|
|
2040
|
+
* line for it — does not repeat every request (#10221). A successful read,
|
|
2041
|
+
* including a legitimate "no settings configured yet" empty result, is NEVER
|
|
2042
|
+
* cached: the next call always re-reads, so a settings write takes effect
|
|
2043
|
+
* immediately (see the cache doc above for why — the dogfood analytics
|
|
2044
|
+
* bucketing test pins this).
|
|
2029
2045
|
*/
|
|
2030
|
-
declare function resolveLocalizationContext(input: ResolveLocalizationInput): Promise<
|
|
2031
|
-
timezone: string;
|
|
2032
|
-
locale: string;
|
|
2033
|
-
currency?: string;
|
|
2034
|
-
}>;
|
|
2046
|
+
declare function resolveLocalizationContext(input: ResolveLocalizationInput): Promise<LocalizationResult>;
|
|
2035
2047
|
|
|
2036
2048
|
/**
|
|
2037
2049
|
* ADR-0069 — authentication-policy session gate.
|
|
@@ -3510,11 +3522,15 @@ declare function createMemoryQueue(): {
|
|
|
3510
3522
|
};
|
|
3511
3523
|
|
|
3512
3524
|
/**
|
|
3513
|
-
* In-memory job
|
|
3525
|
+
* In-memory job registry — schedule/cancel/trigger bookkeeping with NO timer.
|
|
3514
3526
|
*
|
|
3515
|
-
*
|
|
3516
|
-
*
|
|
3517
|
-
*
|
|
3527
|
+
* [#10746] NOT pre-injected by ObjectKernel any more (it used to be, via
|
|
3528
|
+
* `CORE_FALLBACK_FACTORIES`): a fallback must not fake capability (maintainer
|
|
3529
|
+
* ruling 2026-08-22). Advertising a `schedule()` that records and never fires
|
|
3530
|
+
* made every "prefer the platform job service, else own a timer" consumer
|
|
3531
|
+
* take the job-service branch and then silently never run. The export remains
|
|
3532
|
+
* for embedders who deliberately want a manual-trigger job registry — e.g. in
|
|
3533
|
+
* tests that drive handlers via `trigger()` — and have read this docblock.
|
|
3518
3534
|
*
|
|
3519
3535
|
* [#4058] `degraded` (ADR-0076 D12), with the missing half named in the
|
|
3520
3536
|
* message rather than left for a deployer to discover: `trigger()` really runs
|
|
@@ -3666,8 +3682,26 @@ declare function wireAuthoredTranslationSync(ctx: MinimalCtx): void;
|
|
|
3666
3682
|
|
|
3667
3683
|
/**
|
|
3668
3684
|
* Map of core-criticality service names to their in-memory fallback factories.
|
|
3669
|
-
*
|
|
3670
|
-
*
|
|
3685
|
+
* This IS the kernel's pre-injection list: `ObjectKernel.preInjectCoreFallbacks()`
|
|
3686
|
+
* registers an entry for every unprovided `core` service before Phase 2, and
|
|
3687
|
+
* `validateSystemRequirements()` consults the same map as its final check.
|
|
3688
|
+
*
|
|
3689
|
+
* [#10746] `job` is deliberately ABSENT — a fallback must not fake capability
|
|
3690
|
+
* (maintainer ruling 2026-08-22). `createMemoryJob()`'s `schedule()` records a
|
|
3691
|
+
* job and never fires it, so pre-injecting it made every "prefer the platform
|
|
3692
|
+
* job service, else own a timer" consumer take the job-service branch and then
|
|
3693
|
+
* silently never run: `plugin-reports` logged `dispatcher registered with job
|
|
3694
|
+
* service` and dispatched nothing, ever (measured: 0 reads of
|
|
3695
|
+
* `sys_report_schedule` in 5600 ms with the success line present). With no
|
|
3696
|
+
* entry here, `getService('job')` throws when no job plugin is installed,
|
|
3697
|
+
* every consumer's documented no-job-service path becomes reachable (they all
|
|
3698
|
+
* already run on `LiteKernel`, which injects no fallbacks), and the kernel
|
|
3699
|
+
* says the absence out loud at boot: `validateSystemRequirements()` warns
|
|
3700
|
+
* "Core service missing, functionality may be degraded: job". Do NOT re-add
|
|
3701
|
+
* the entry to quiet that warning — install `@objectstack/service-job`, or
|
|
3702
|
+
* register a real scheduler, instead. `createMemoryJob` stays exported below
|
|
3703
|
+
* for embedders who deliberately want a manual-trigger job registry and have
|
|
3704
|
+
* read its docblock.
|
|
3671
3705
|
*/
|
|
3672
3706
|
declare const CORE_FALLBACK_FACTORIES: Record<string, () => Record<string, any>>;
|
|
3673
3707
|
|
package/dist/index.d.ts
CHANGED
|
@@ -320,12 +320,14 @@ declare class ObjectKernel {
|
|
|
320
320
|
* as well: if the hook never settles and nothing else keeps the loop alive,
|
|
321
321
|
* Node exits before the timer can fire and the timeout is never reported.
|
|
322
322
|
* The guard has to stay ref'd exactly as long as the race is undecided,
|
|
323
|
-
* which is what
|
|
323
|
+
* which is what clearing on settle expresses.
|
|
324
324
|
*
|
|
325
|
-
*
|
|
326
|
-
*
|
|
327
|
-
*
|
|
328
|
-
*
|
|
325
|
+
* Clearing the timer was only half of it, though (#10604): the promise the
|
|
326
|
+
* race still holds a reaction on has to SETTLE, or it and that reaction are
|
|
327
|
+
* retained past the end of the run — two leaking promises per boot, which
|
|
328
|
+
* is what `vitest --detectAsyncLeaks` names here. Both halves now live in
|
|
329
|
+
* `TimeoutGuard.reclaim()`, shared with `shutdown()`, so the two sites
|
|
330
|
+
* cannot drift into doing one half each again.
|
|
329
331
|
*/
|
|
330
332
|
private raceStartupTimeout;
|
|
331
333
|
/**
|
|
@@ -2021,17 +2023,27 @@ interface ResolveLocalizationInput {
|
|
|
2021
2023
|
tenantId?: string;
|
|
2022
2024
|
userId?: string;
|
|
2023
2025
|
}
|
|
2026
|
+
type LocalizationResult = {
|
|
2027
|
+
timezone: string;
|
|
2028
|
+
locale: string;
|
|
2029
|
+
currency?: string;
|
|
2030
|
+
};
|
|
2024
2031
|
/**
|
|
2025
2032
|
* Resolve workspace localization defaults (reference `timezone` / `locale` /
|
|
2026
2033
|
* `currency`). Canonical path is the `localization` SettingsManifest (cascade:
|
|
2027
2034
|
* platform default → global → tenant); falls back to direct tenant-scoped
|
|
2028
2035
|
* `sys_setting` rows, then the built-ins `UTC` / `en-US`. Never throws.
|
|
2036
|
+
*
|
|
2037
|
+
* A read that fails outright (backend fault — table missing, connection
|
|
2038
|
+
* refused, etc.) is memoized for {@link LOCALIZATION_FAILURE_CACHE_TTL_MS}
|
|
2039
|
+
* per `(ql, tenantId, userId)` so the failing query — and the driver's log
|
|
2040
|
+
* line for it — does not repeat every request (#10221). A successful read,
|
|
2041
|
+
* including a legitimate "no settings configured yet" empty result, is NEVER
|
|
2042
|
+
* cached: the next call always re-reads, so a settings write takes effect
|
|
2043
|
+
* immediately (see the cache doc above for why — the dogfood analytics
|
|
2044
|
+
* bucketing test pins this).
|
|
2029
2045
|
*/
|
|
2030
|
-
declare function resolveLocalizationContext(input: ResolveLocalizationInput): Promise<
|
|
2031
|
-
timezone: string;
|
|
2032
|
-
locale: string;
|
|
2033
|
-
currency?: string;
|
|
2034
|
-
}>;
|
|
2046
|
+
declare function resolveLocalizationContext(input: ResolveLocalizationInput): Promise<LocalizationResult>;
|
|
2035
2047
|
|
|
2036
2048
|
/**
|
|
2037
2049
|
* ADR-0069 — authentication-policy session gate.
|
|
@@ -3510,11 +3522,15 @@ declare function createMemoryQueue(): {
|
|
|
3510
3522
|
};
|
|
3511
3523
|
|
|
3512
3524
|
/**
|
|
3513
|
-
* In-memory job
|
|
3525
|
+
* In-memory job registry — schedule/cancel/trigger bookkeeping with NO timer.
|
|
3514
3526
|
*
|
|
3515
|
-
*
|
|
3516
|
-
*
|
|
3517
|
-
*
|
|
3527
|
+
* [#10746] NOT pre-injected by ObjectKernel any more (it used to be, via
|
|
3528
|
+
* `CORE_FALLBACK_FACTORIES`): a fallback must not fake capability (maintainer
|
|
3529
|
+
* ruling 2026-08-22). Advertising a `schedule()` that records and never fires
|
|
3530
|
+
* made every "prefer the platform job service, else own a timer" consumer
|
|
3531
|
+
* take the job-service branch and then silently never run. The export remains
|
|
3532
|
+
* for embedders who deliberately want a manual-trigger job registry — e.g. in
|
|
3533
|
+
* tests that drive handlers via `trigger()` — and have read this docblock.
|
|
3518
3534
|
*
|
|
3519
3535
|
* [#4058] `degraded` (ADR-0076 D12), with the missing half named in the
|
|
3520
3536
|
* message rather than left for a deployer to discover: `trigger()` really runs
|
|
@@ -3666,8 +3682,26 @@ declare function wireAuthoredTranslationSync(ctx: MinimalCtx): void;
|
|
|
3666
3682
|
|
|
3667
3683
|
/**
|
|
3668
3684
|
* Map of core-criticality service names to their in-memory fallback factories.
|
|
3669
|
-
*
|
|
3670
|
-
*
|
|
3685
|
+
* This IS the kernel's pre-injection list: `ObjectKernel.preInjectCoreFallbacks()`
|
|
3686
|
+
* registers an entry for every unprovided `core` service before Phase 2, and
|
|
3687
|
+
* `validateSystemRequirements()` consults the same map as its final check.
|
|
3688
|
+
*
|
|
3689
|
+
* [#10746] `job` is deliberately ABSENT — a fallback must not fake capability
|
|
3690
|
+
* (maintainer ruling 2026-08-22). `createMemoryJob()`'s `schedule()` records a
|
|
3691
|
+
* job and never fires it, so pre-injecting it made every "prefer the platform
|
|
3692
|
+
* job service, else own a timer" consumer take the job-service branch and then
|
|
3693
|
+
* silently never run: `plugin-reports` logged `dispatcher registered with job
|
|
3694
|
+
* service` and dispatched nothing, ever (measured: 0 reads of
|
|
3695
|
+
* `sys_report_schedule` in 5600 ms with the success line present). With no
|
|
3696
|
+
* entry here, `getService('job')` throws when no job plugin is installed,
|
|
3697
|
+
* every consumer's documented no-job-service path becomes reachable (they all
|
|
3698
|
+
* already run on `LiteKernel`, which injects no fallbacks), and the kernel
|
|
3699
|
+
* says the absence out loud at boot: `validateSystemRequirements()` warns
|
|
3700
|
+
* "Core service missing, functionality may be degraded: job". Do NOT re-add
|
|
3701
|
+
* the entry to quiet that warning — install `@objectstack/service-job`, or
|
|
3702
|
+
* register a real scheduler, instead. `createMemoryJob` stays exported below
|
|
3703
|
+
* for embedders who deliberately want a manual-trigger job registry and have
|
|
3704
|
+
* read its docblock.
|
|
3671
3705
|
*/
|
|
3672
3706
|
declare const CORE_FALLBACK_FACTORIES: Record<string, () => Record<string, any>>;
|
|
3673
3707
|
|
package/dist/index.js
CHANGED
|
@@ -1355,35 +1355,6 @@ function createMemoryQueue() {
|
|
|
1355
1355
|
};
|
|
1356
1356
|
}
|
|
1357
1357
|
|
|
1358
|
-
// src/fallbacks/memory-job.ts
|
|
1359
|
-
function createMemoryJob() {
|
|
1360
|
-
const jobs = /* @__PURE__ */ new Map();
|
|
1361
|
-
return {
|
|
1362
|
-
__serviceInfo: {
|
|
1363
|
-
status: "degraded",
|
|
1364
|
-
handlerReady: false,
|
|
1365
|
-
message: "In-process job registry \u2014 trigger() runs handlers, but scheduled jobs never fire on their own (no timer). Register a job plugin (e.g. Agenda) for real scheduling."
|
|
1366
|
-
},
|
|
1367
|
-
_serviceName: "job",
|
|
1368
|
-
async schedule(name, schedule, handler) {
|
|
1369
|
-
jobs.set(name, { schedule, handler });
|
|
1370
|
-
},
|
|
1371
|
-
async cancel(name) {
|
|
1372
|
-
jobs.delete(name);
|
|
1373
|
-
},
|
|
1374
|
-
async trigger(name, data) {
|
|
1375
|
-
const job = jobs.get(name);
|
|
1376
|
-
if (job?.handler) await job.handler({ jobId: name, data });
|
|
1377
|
-
},
|
|
1378
|
-
async getExecutions() {
|
|
1379
|
-
return [];
|
|
1380
|
-
},
|
|
1381
|
-
async listJobs() {
|
|
1382
|
-
return [...jobs.keys()];
|
|
1383
|
-
}
|
|
1384
|
-
};
|
|
1385
|
-
}
|
|
1386
|
-
|
|
1387
1358
|
// src/fallbacks/memory-i18n.ts
|
|
1388
1359
|
import { normalizeSupportedLocales } from "@objectstack/spec/system";
|
|
1389
1360
|
function deepMerge(target, source) {
|
|
@@ -1611,6 +1582,35 @@ function createMemoryMetadata() {
|
|
|
1611
1582
|
};
|
|
1612
1583
|
}
|
|
1613
1584
|
|
|
1585
|
+
// src/fallbacks/memory-job.ts
|
|
1586
|
+
function createMemoryJob() {
|
|
1587
|
+
const jobs = /* @__PURE__ */ new Map();
|
|
1588
|
+
return {
|
|
1589
|
+
__serviceInfo: {
|
|
1590
|
+
status: "degraded",
|
|
1591
|
+
handlerReady: false,
|
|
1592
|
+
message: "In-process job registry \u2014 trigger() runs handlers, but scheduled jobs never fire on their own (no timer). Register a job plugin (e.g. Agenda) for real scheduling."
|
|
1593
|
+
},
|
|
1594
|
+
_serviceName: "job",
|
|
1595
|
+
async schedule(name, schedule, handler) {
|
|
1596
|
+
jobs.set(name, { schedule, handler });
|
|
1597
|
+
},
|
|
1598
|
+
async cancel(name) {
|
|
1599
|
+
jobs.delete(name);
|
|
1600
|
+
},
|
|
1601
|
+
async trigger(name, data) {
|
|
1602
|
+
const job = jobs.get(name);
|
|
1603
|
+
if (job?.handler) await job.handler({ jobId: name, data });
|
|
1604
|
+
},
|
|
1605
|
+
async getExecutions() {
|
|
1606
|
+
return [];
|
|
1607
|
+
},
|
|
1608
|
+
async listJobs() {
|
|
1609
|
+
return [...jobs.keys()];
|
|
1610
|
+
}
|
|
1611
|
+
};
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
1614
|
// src/fallbacks/authored-translation-sync.ts
|
|
1615
1615
|
import { LEGACY_OBJECT_FIRST_KEYS } from "@objectstack/spec/system";
|
|
1616
1616
|
var OWNER_PROP = "__authoredTranslationSyncOwner";
|
|
@@ -1743,7 +1743,6 @@ var CORE_FALLBACK_FACTORIES = {
|
|
|
1743
1743
|
metadata: createMemoryMetadata,
|
|
1744
1744
|
cache: createMemoryCache,
|
|
1745
1745
|
queue: createMemoryQueue,
|
|
1746
|
-
job: createMemoryJob,
|
|
1747
1746
|
i18n: createMemoryI18n
|
|
1748
1747
|
};
|
|
1749
1748
|
|
|
@@ -1767,6 +1766,48 @@ function registerPluginByName(registry, plugin, logger) {
|
|
|
1767
1766
|
return previous;
|
|
1768
1767
|
}
|
|
1769
1768
|
|
|
1769
|
+
// src/timeout-guard.ts
|
|
1770
|
+
var TimeoutGuard = class {
|
|
1771
|
+
constructor(timeoutMs, createTimeoutError) {
|
|
1772
|
+
/**
|
|
1773
|
+
* Settles `expiry` without a value. `Promise<never>` has no resolvable
|
|
1774
|
+
* value in the type system, but settling it is the entire point: it is
|
|
1775
|
+
* only ever called from `reclaim()`, i.e. after the race it guarded has
|
|
1776
|
+
* already been decided, so the resolution is discarded by construction and
|
|
1777
|
+
* can never become a race winner. The cast localises that argument here
|
|
1778
|
+
* rather than pushing a lie into every caller's return type.
|
|
1779
|
+
*/
|
|
1780
|
+
this.settleExpiry = () => {
|
|
1781
|
+
};
|
|
1782
|
+
this.expiry = new Promise((resolve, reject) => {
|
|
1783
|
+
this.settleExpiry = resolve;
|
|
1784
|
+
this.timer = setTimeout(() => reject(createTimeoutError()), timeoutMs);
|
|
1785
|
+
});
|
|
1786
|
+
}
|
|
1787
|
+
/**
|
|
1788
|
+
* Reclaim the guard once the race it protects has been decided. Both
|
|
1789
|
+
* halves, always: the timer is cleared so it cannot fire against a
|
|
1790
|
+
* lifecycle phase that is already over, and `expiry` is settled so neither
|
|
1791
|
+
* it nor the race's reaction on it is retained.
|
|
1792
|
+
*
|
|
1793
|
+
* Idempotent — `clearTimeout` on a cleared handle and a second resolve on
|
|
1794
|
+
* a settled promise are both no-ops.
|
|
1795
|
+
*/
|
|
1796
|
+
reclaim() {
|
|
1797
|
+
clearTimeout(this.timer);
|
|
1798
|
+
this.timer = void 0;
|
|
1799
|
+
this.settleExpiry();
|
|
1800
|
+
}
|
|
1801
|
+
};
|
|
1802
|
+
async function raceWithTimeout(operation, timeoutMs, createTimeoutError) {
|
|
1803
|
+
const guard = new TimeoutGuard(timeoutMs, createTimeoutError);
|
|
1804
|
+
try {
|
|
1805
|
+
return await Promise.race([operation, guard.expiry]);
|
|
1806
|
+
} finally {
|
|
1807
|
+
guard.reclaim();
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1770
1811
|
// src/kernel.ts
|
|
1771
1812
|
var ObjectKernel = class {
|
|
1772
1813
|
constructor(config = {}) {
|
|
@@ -2037,14 +2078,11 @@ var ObjectKernel = class {
|
|
|
2037
2078
|
this.logger.info("Graceful shutdown started");
|
|
2038
2079
|
const shutdownTimeoutError = new Error("Shutdown timeout exceeded");
|
|
2039
2080
|
try {
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
if (t.unref) t.unref();
|
|
2046
|
-
});
|
|
2047
|
-
await Promise.race([shutdownPromise, timeoutPromise]);
|
|
2081
|
+
await raceWithTimeout(
|
|
2082
|
+
this.performShutdown(),
|
|
2083
|
+
this.config.shutdownTimeout,
|
|
2084
|
+
() => shutdownTimeoutError
|
|
2085
|
+
);
|
|
2048
2086
|
this.state = "stopped";
|
|
2049
2087
|
this.logger.info("\u2705 Graceful shutdown complete");
|
|
2050
2088
|
} catch (error) {
|
|
@@ -2162,25 +2200,17 @@ var ObjectKernel = class {
|
|
|
2162
2200
|
* as well: if the hook never settles and nothing else keeps the loop alive,
|
|
2163
2201
|
* Node exits before the timer can fire and the timeout is never reported.
|
|
2164
2202
|
* The guard has to stay ref'd exactly as long as the race is undecided,
|
|
2165
|
-
* which is what
|
|
2203
|
+
* which is what clearing on settle expresses.
|
|
2166
2204
|
*
|
|
2167
|
-
*
|
|
2168
|
-
*
|
|
2169
|
-
*
|
|
2170
|
-
*
|
|
2205
|
+
* Clearing the timer was only half of it, though (#10604): the promise the
|
|
2206
|
+
* race still holds a reaction on has to SETTLE, or it and that reaction are
|
|
2207
|
+
* retained past the end of the run — two leaking promises per boot, which
|
|
2208
|
+
* is what `vitest --detectAsyncLeaks` names here. Both halves now live in
|
|
2209
|
+
* `TimeoutGuard.reclaim()`, shared with `shutdown()`, so the two sites
|
|
2210
|
+
* cannot drift into doing one half each again.
|
|
2171
2211
|
*/
|
|
2172
2212
|
async raceStartupTimeout(operation, timeout, message) {
|
|
2173
|
-
|
|
2174
|
-
const timeoutPromise = new Promise((_, reject) => {
|
|
2175
|
-
guard = setTimeout(() => {
|
|
2176
|
-
reject(new Error(message));
|
|
2177
|
-
}, timeout);
|
|
2178
|
-
});
|
|
2179
|
-
try {
|
|
2180
|
-
return await Promise.race([operation, timeoutPromise]);
|
|
2181
|
-
} finally {
|
|
2182
|
-
clearTimeout(guard);
|
|
2183
|
-
}
|
|
2213
|
+
return raceWithTimeout(operation, timeout, () => new Error(message));
|
|
2184
2214
|
}
|
|
2185
2215
|
/**
|
|
2186
2216
|
* Whether a service is resolvable on this kernel right now — direct
|
|
@@ -4396,10 +4426,11 @@ function safeJsonParse2(s, fallback) {
|
|
|
4396
4426
|
return fallback;
|
|
4397
4427
|
}
|
|
4398
4428
|
}
|
|
4399
|
-
async function tryFind(ql, object, where, limit = 100) {
|
|
4429
|
+
async function tryFind(ql, object, where, limit = 100, organizationId) {
|
|
4400
4430
|
if (!ql || typeof ql.find !== "function") return [];
|
|
4401
4431
|
try {
|
|
4402
|
-
|
|
4432
|
+
const context = organizationId ? { isSystem: true, tenantId: organizationId } : { isSystem: true };
|
|
4433
|
+
let rows = await ql.find(object, { where, limit, context });
|
|
4403
4434
|
if (rows && rows.value) rows = rows.value;
|
|
4404
4435
|
return Array.isArray(rows) ? rows : [];
|
|
4405
4436
|
} catch {
|
|
@@ -4497,12 +4528,19 @@ async function resolveUserAuthzGrants(ql, userId, opts = {}) {
|
|
|
4497
4528
|
}
|
|
4498
4529
|
return userRow;
|
|
4499
4530
|
};
|
|
4531
|
+
const needsUserRow = !grants.email || !grants.permissions.includes("ai_seat");
|
|
4532
|
+
const [, members, userPositionRows, orgMembersLeg, upsRowsAll] = await Promise.all([
|
|
4533
|
+
needsUserRow ? getUserRow() : Promise.resolve(void 0),
|
|
4534
|
+
tryFind(ql, "sys_member", { user_id: userId }, 200),
|
|
4535
|
+
tryFind(ql, "sys_user_position", { user_id: userId }, 200),
|
|
4536
|
+
tenantId ? tryFind(ql, "sys_member", { organization_id: tenantId }, 1e3) : Promise.resolve([]),
|
|
4537
|
+
tryFind(ql, "sys_user_permission_set", { user_id: userId }, 100)
|
|
4538
|
+
]);
|
|
4500
4539
|
if (!grants.email) {
|
|
4501
4540
|
const u = await getUserRow();
|
|
4502
4541
|
if (u?.email) grants.email = String(u.email);
|
|
4503
4542
|
}
|
|
4504
4543
|
const nowMs = opts.nowMs ?? Date.now();
|
|
4505
|
-
const members = await tryFind(ql, "sys_member", { user_id: userId }, 200);
|
|
4506
4544
|
const accessibleOrgIds = /* @__PURE__ */ new Set();
|
|
4507
4545
|
for (const m of members) {
|
|
4508
4546
|
if (!isGrantActive(m, nowMs)) continue;
|
|
@@ -4510,7 +4548,9 @@ async function resolveUserAuthzGrants(ql, userId, opts = {}) {
|
|
|
4510
4548
|
if (typeof org === "string" && org) accessibleOrgIds.add(org);
|
|
4511
4549
|
}
|
|
4512
4550
|
grants.accessible_org_ids = Array.from(accessibleOrgIds);
|
|
4513
|
-
const activeMembers =
|
|
4551
|
+
const activeMembers = members.filter(
|
|
4552
|
+
(m) => isGrantActive(m, nowMs) && (!tenantId || (m.organization_id ?? m.organizationId) === tenantId)
|
|
4553
|
+
);
|
|
4514
4554
|
for (const m of activeMembers) {
|
|
4515
4555
|
if (m.role && typeof m.role === "string") {
|
|
4516
4556
|
for (const raw of m.role.split(",").map((s) => s.trim()).filter(Boolean)) {
|
|
@@ -4519,7 +4559,6 @@ async function resolveUserAuthzGrants(ql, userId, opts = {}) {
|
|
|
4519
4559
|
}
|
|
4520
4560
|
}
|
|
4521
4561
|
}
|
|
4522
|
-
const userPositionRows = await tryFind(ql, "sys_user_position", { user_id: userId }, 200);
|
|
4523
4562
|
for (const ur of userPositionRows) {
|
|
4524
4563
|
const org = ur.organization_id ?? null;
|
|
4525
4564
|
if (org && tenantId && org !== tenantId) continue;
|
|
@@ -4528,14 +4567,13 @@ async function resolveUserAuthzGrants(ql, userId, opts = {}) {
|
|
|
4528
4567
|
if (typeof r === "string" && r && !grants.positions.includes(r)) grants.positions.push(r);
|
|
4529
4568
|
}
|
|
4530
4569
|
if (tenantId) {
|
|
4531
|
-
const orgMembers =
|
|
4570
|
+
const orgMembers = orgMembersLeg;
|
|
4532
4571
|
const ids = new Set(
|
|
4533
4572
|
orgMembers.map((m) => m.user_id ?? m.userId).filter((v) => typeof v === "string" && v.length > 0)
|
|
4534
4573
|
);
|
|
4535
4574
|
ids.add(userId);
|
|
4536
4575
|
grants.org_user_ids = Array.from(ids);
|
|
4537
4576
|
}
|
|
4538
|
-
const upsRowsAll = await tryFind(ql, "sys_user_permission_set", { user_id: userId }, 100);
|
|
4539
4577
|
const upsRows = upsRowsAll.filter((r) => isGrantActive(r, nowMs));
|
|
4540
4578
|
const psIds = new Set(
|
|
4541
4579
|
upsRows.filter((r) => {
|
|
@@ -4549,7 +4587,7 @@ async function resolveUserAuthzGrants(ql, userId, opts = {}) {
|
|
|
4549
4587
|
let hasPlatformAdminGrant = false;
|
|
4550
4588
|
if (!grants.positions.includes("everyone")) grants.positions.push("everyone");
|
|
4551
4589
|
if (grants.positions.length > 0) {
|
|
4552
|
-
const positionRows = await tryFind(ql, "sys_position", { name: { $in: grants.positions } },
|
|
4590
|
+
const positionRows = await tryFind(ql, "sys_position", { name: { $in: grants.positions } }, 200, tenantId);
|
|
4553
4591
|
const deactivatedNames = new Set(
|
|
4554
4592
|
positionRows.filter((r) => !isRowActive(r)).map((r) => r.name).filter(Boolean)
|
|
4555
4593
|
);
|
|
@@ -4627,34 +4665,89 @@ function coerceCurrency(value) {
|
|
|
4627
4665
|
const s = typeof value === "string" ? value.trim().toUpperCase() : "";
|
|
4628
4666
|
return /^[A-Z]{3}$/.test(s) ? s : void 0;
|
|
4629
4667
|
}
|
|
4668
|
+
var LOCALIZATION_FAILURE_CACHE_TTL_MS = 3e4;
|
|
4669
|
+
var localizationFailureCache = /* @__PURE__ */ new WeakMap();
|
|
4630
4670
|
async function resolveLocalizationContext(input) {
|
|
4671
|
+
const { ql, tenantId, userId } = input;
|
|
4672
|
+
const cacheKey = `${tenantId ?? ""}|${userId ?? ""}`;
|
|
4673
|
+
if (ql && typeof ql === "object") {
|
|
4674
|
+
const hit = localizationFailureCache.get(ql)?.get(cacheKey);
|
|
4675
|
+
if (hit && hit.expiresAt > Date.now()) return hit.value;
|
|
4676
|
+
}
|
|
4677
|
+
const { value, failed } = await resolveLocalizationContextUncached(input);
|
|
4678
|
+
if (failed && ql && typeof ql === "object") {
|
|
4679
|
+
const bucket = localizationFailureCache.get(ql) ?? /* @__PURE__ */ new Map();
|
|
4680
|
+
bucket.set(cacheKey, { value, expiresAt: Date.now() + LOCALIZATION_FAILURE_CACHE_TTL_MS });
|
|
4681
|
+
localizationFailureCache.set(ql, bucket);
|
|
4682
|
+
}
|
|
4683
|
+
return value;
|
|
4684
|
+
}
|
|
4685
|
+
async function resolveLocalizationContextUncached(input) {
|
|
4631
4686
|
const { ql, settings, tenantId, userId } = input;
|
|
4687
|
+
let failed = false;
|
|
4632
4688
|
try {
|
|
4633
4689
|
if (settings && typeof settings.get === "function") {
|
|
4634
4690
|
const sctx = { tenantId, userId };
|
|
4635
|
-
|
|
4636
|
-
|
|
4637
|
-
|
|
4638
|
-
|
|
4639
|
-
|
|
4691
|
+
let tzRes;
|
|
4692
|
+
let localeRes;
|
|
4693
|
+
let currencyRes;
|
|
4694
|
+
if (typeof settings.getMany === "function") {
|
|
4695
|
+
try {
|
|
4696
|
+
const many = await settings.getMany("localization", ["timezone", "locale", "currency"], sctx);
|
|
4697
|
+
tzRes = many.timezone;
|
|
4698
|
+
localeRes = many.locale;
|
|
4699
|
+
currencyRes = many.currency;
|
|
4700
|
+
} catch {
|
|
4701
|
+
failed = true;
|
|
4702
|
+
}
|
|
4703
|
+
} else {
|
|
4704
|
+
[tzRes, localeRes, currencyRes] = await Promise.all([
|
|
4705
|
+
settings.get("localization", "timezone", sctx).catch(() => {
|
|
4706
|
+
failed = true;
|
|
4707
|
+
return void 0;
|
|
4708
|
+
}),
|
|
4709
|
+
settings.get("localization", "locale", sctx).catch(() => {
|
|
4710
|
+
failed = true;
|
|
4711
|
+
return void 0;
|
|
4712
|
+
}),
|
|
4713
|
+
settings.get("localization", "currency", sctx).catch(() => {
|
|
4714
|
+
failed = true;
|
|
4715
|
+
return void 0;
|
|
4716
|
+
})
|
|
4717
|
+
]);
|
|
4718
|
+
}
|
|
4640
4719
|
const tz = coerceTimeZone(tzRes?.value);
|
|
4641
4720
|
const locale = coerceLocale(localeRes?.value);
|
|
4642
4721
|
const currency = coerceCurrency(currencyRes?.value);
|
|
4643
|
-
if (tz || locale || currency)
|
|
4722
|
+
if (tz || locale || currency) {
|
|
4723
|
+
return { value: { timezone: tz ?? "UTC", locale: locale ?? "en-US", currency }, failed: false };
|
|
4724
|
+
}
|
|
4644
4725
|
}
|
|
4645
4726
|
} catch {
|
|
4727
|
+
failed = true;
|
|
4728
|
+
}
|
|
4729
|
+
let rows = [];
|
|
4730
|
+
if (ql && typeof ql.find === "function") {
|
|
4731
|
+
try {
|
|
4732
|
+
let result = await ql.find("sys_setting", {
|
|
4733
|
+
where: { namespace: "localization", key: { $in: ["timezone", "locale", "currency"] }, scope: "tenant" },
|
|
4734
|
+
limit: 10,
|
|
4735
|
+
context: { isSystem: true }
|
|
4736
|
+
});
|
|
4737
|
+
if (result && result.value) result = result.value;
|
|
4738
|
+
rows = Array.isArray(result) ? result : [];
|
|
4739
|
+
} catch {
|
|
4740
|
+
failed = true;
|
|
4741
|
+
}
|
|
4646
4742
|
}
|
|
4647
|
-
const rows = await tryFind(
|
|
4648
|
-
ql,
|
|
4649
|
-
"sys_setting",
|
|
4650
|
-
{ namespace: "localization", key: { $in: ["timezone", "locale", "currency"] }, scope: "tenant" },
|
|
4651
|
-
10
|
|
4652
|
-
);
|
|
4653
4743
|
const valueOf = (k) => rows.find((r) => r.key === k)?.value;
|
|
4654
4744
|
return {
|
|
4655
|
-
|
|
4656
|
-
|
|
4657
|
-
|
|
4745
|
+
value: {
|
|
4746
|
+
timezone: coerceTimeZone(valueOf("timezone")) ?? "UTC",
|
|
4747
|
+
locale: coerceLocale(valueOf("locale")) ?? "en-US",
|
|
4748
|
+
currency: coerceCurrency(valueOf("currency"))
|
|
4749
|
+
},
|
|
4750
|
+
failed
|
|
4658
4751
|
};
|
|
4659
4752
|
}
|
|
4660
4753
|
|