@objectstack/core 4.0.4 → 4.0.5
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/README.md +95 -10
- package/dist/index.cjs +169 -507
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +24 -223
- package/dist/index.d.ts +24 -223
- package/dist/index.js +175 -505
- package/dist/index.js.map +1 -1
- package/dist/logger.cjs +177 -0
- package/dist/logger.cjs.map +1 -0
- package/dist/logger.d.cts +26 -0
- package/dist/logger.d.ts +26 -0
- package/dist/logger.js +158 -0
- package/dist/logger.js.map +1 -0
- package/package.json +36 -15
- package/.turbo/turbo-build.log +0 -22
- package/ADVANCED_FEATURES.md +0 -380
- package/API_REGISTRY.md +0 -392
- package/CHANGELOG.md +0 -472
- package/PHASE2_IMPLEMENTATION.md +0 -388
- package/REFACTORING_SUMMARY.md +0 -40
- package/examples/api-registry-example.ts +0 -559
- package/examples/kernel-features-example.ts +0 -311
- package/examples/phase2-integration.ts +0 -357
- package/src/api-registry-plugin.test.ts +0 -393
- package/src/api-registry-plugin.ts +0 -89
- package/src/api-registry.test.ts +0 -1089
- package/src/api-registry.ts +0 -739
- package/src/contracts/data-engine.ts +0 -57
- package/src/contracts/http-server.ts +0 -151
- package/src/contracts/logger.ts +0 -72
- package/src/dependency-resolver.test.ts +0 -287
- package/src/dependency-resolver.ts +0 -390
- package/src/fallbacks/fallbacks.test.ts +0 -281
- package/src/fallbacks/index.ts +0 -26
- package/src/fallbacks/memory-cache.ts +0 -34
- package/src/fallbacks/memory-i18n.ts +0 -112
- package/src/fallbacks/memory-job.ts +0 -23
- package/src/fallbacks/memory-metadata.ts +0 -50
- package/src/fallbacks/memory-queue.ts +0 -28
- package/src/health-monitor.test.ts +0 -81
- package/src/health-monitor.ts +0 -318
- package/src/hot-reload.ts +0 -382
- package/src/index.ts +0 -50
- package/src/kernel-base.ts +0 -273
- package/src/kernel.test.ts +0 -624
- package/src/kernel.ts +0 -631
- package/src/lite-kernel.test.ts +0 -248
- package/src/lite-kernel.ts +0 -137
- package/src/logger.test.ts +0 -116
- package/src/logger.ts +0 -355
- package/src/namespace-resolver.test.ts +0 -130
- package/src/namespace-resolver.ts +0 -188
- package/src/package-manager.test.ts +0 -225
- package/src/package-manager.ts +0 -428
- package/src/plugin-loader.test.ts +0 -421
- package/src/plugin-loader.ts +0 -484
- package/src/qa/adapter.ts +0 -16
- package/src/qa/http-adapter.ts +0 -116
- package/src/qa/index.ts +0 -5
- package/src/qa/runner.ts +0 -189
- package/src/security/index.ts +0 -50
- package/src/security/permission-manager.test.ts +0 -256
- package/src/security/permission-manager.ts +0 -338
- package/src/security/plugin-config-validator.test.ts +0 -276
- package/src/security/plugin-config-validator.ts +0 -193
- package/src/security/plugin-permission-enforcer.test.ts +0 -251
- package/src/security/plugin-permission-enforcer.ts +0 -436
- package/src/security/plugin-signature-verifier.ts +0 -403
- package/src/security/sandbox-runtime.ts +0 -462
- package/src/security/security-scanner.ts +0 -367
- package/src/types.ts +0 -120
- package/src/utils/env.test.ts +0 -62
- package/src/utils/env.ts +0 -53
- package/tsconfig.json +0 -10
- package/vitest.config.ts +0 -10
|
@@ -1,390 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
-
|
|
3
|
-
import type {
|
|
4
|
-
SemanticVersion,
|
|
5
|
-
VersionConstraint,
|
|
6
|
-
CompatibilityLevel,
|
|
7
|
-
DependencyConflict
|
|
8
|
-
} from '@objectstack/spec/kernel';
|
|
9
|
-
import type { ObjectLogger } from './logger.js';
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Semantic Version Parser and Comparator
|
|
13
|
-
*
|
|
14
|
-
* Implements semantic versioning comparison and constraint matching
|
|
15
|
-
*/
|
|
16
|
-
export class SemanticVersionManager {
|
|
17
|
-
/**
|
|
18
|
-
* Parse a version string into semantic version components
|
|
19
|
-
*/
|
|
20
|
-
static parse(versionStr: string): SemanticVersion {
|
|
21
|
-
// Remove 'v' prefix if present
|
|
22
|
-
const cleanVersion = versionStr.replace(/^v/, '');
|
|
23
|
-
|
|
24
|
-
// Match semver pattern: major.minor.patch[-prerelease][+build]
|
|
25
|
-
const match = cleanVersion.match(
|
|
26
|
-
/^(\d+)\.(\d+)\.(\d+)(?:-([a-zA-Z0-9.-]+))?(?:\+([a-zA-Z0-9.-]+))?$/
|
|
27
|
-
);
|
|
28
|
-
|
|
29
|
-
if (!match) {
|
|
30
|
-
throw new Error(`Invalid semantic version: ${versionStr}`);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
return {
|
|
34
|
-
major: parseInt(match[1], 10),
|
|
35
|
-
minor: parseInt(match[2], 10),
|
|
36
|
-
patch: parseInt(match[3], 10),
|
|
37
|
-
preRelease: match[4],
|
|
38
|
-
build: match[5],
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Convert semantic version back to string
|
|
44
|
-
*/
|
|
45
|
-
static toString(version: SemanticVersion): string {
|
|
46
|
-
let str = `${version.major}.${version.minor}.${version.patch}`;
|
|
47
|
-
if (version.preRelease) {
|
|
48
|
-
str += `-${version.preRelease}`;
|
|
49
|
-
}
|
|
50
|
-
if (version.build) {
|
|
51
|
-
str += `+${version.build}`;
|
|
52
|
-
}
|
|
53
|
-
return str;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Compare two semantic versions
|
|
58
|
-
* Returns: -1 if a < b, 0 if a === b, 1 if a > b
|
|
59
|
-
*/
|
|
60
|
-
static compare(a: SemanticVersion, b: SemanticVersion): number {
|
|
61
|
-
// Compare major, minor, patch
|
|
62
|
-
if (a.major !== b.major) return a.major - b.major;
|
|
63
|
-
if (a.minor !== b.minor) return a.minor - b.minor;
|
|
64
|
-
if (a.patch !== b.patch) return a.patch - b.patch;
|
|
65
|
-
|
|
66
|
-
// Pre-release versions have lower precedence
|
|
67
|
-
if (a.preRelease && !b.preRelease) return -1;
|
|
68
|
-
if (!a.preRelease && b.preRelease) return 1;
|
|
69
|
-
|
|
70
|
-
// Compare pre-release versions
|
|
71
|
-
if (a.preRelease && b.preRelease) {
|
|
72
|
-
return a.preRelease.localeCompare(b.preRelease);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
return 0;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Check if version satisfies constraint
|
|
80
|
-
*/
|
|
81
|
-
static satisfies(version: SemanticVersion, constraint: VersionConstraint): boolean {
|
|
82
|
-
const constraintStr = constraint as string;
|
|
83
|
-
|
|
84
|
-
// Any version
|
|
85
|
-
if (constraintStr === '*' || constraintStr === 'latest') {
|
|
86
|
-
return true;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// Exact version
|
|
90
|
-
if (/^[\d.]+$/.test(constraintStr)) {
|
|
91
|
-
const exact = this.parse(constraintStr);
|
|
92
|
-
return this.compare(version, exact) === 0;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// Caret range (^): Compatible with version
|
|
96
|
-
if (constraintStr.startsWith('^')) {
|
|
97
|
-
const base = this.parse(constraintStr.slice(1));
|
|
98
|
-
return (
|
|
99
|
-
version.major === base.major &&
|
|
100
|
-
this.compare(version, base) >= 0
|
|
101
|
-
);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// Tilde range (~): Approximately equivalent
|
|
105
|
-
if (constraintStr.startsWith('~')) {
|
|
106
|
-
const base = this.parse(constraintStr.slice(1));
|
|
107
|
-
return (
|
|
108
|
-
version.major === base.major &&
|
|
109
|
-
version.minor === base.minor &&
|
|
110
|
-
this.compare(version, base) >= 0
|
|
111
|
-
);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// Greater than or equal
|
|
115
|
-
if (constraintStr.startsWith('>=')) {
|
|
116
|
-
const base = this.parse(constraintStr.slice(2));
|
|
117
|
-
return this.compare(version, base) >= 0;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// Greater than
|
|
121
|
-
if (constraintStr.startsWith('>')) {
|
|
122
|
-
const base = this.parse(constraintStr.slice(1));
|
|
123
|
-
return this.compare(version, base) > 0;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// Less than or equal
|
|
127
|
-
if (constraintStr.startsWith('<=')) {
|
|
128
|
-
const base = this.parse(constraintStr.slice(2));
|
|
129
|
-
return this.compare(version, base) <= 0;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// Less than
|
|
133
|
-
if (constraintStr.startsWith('<')) {
|
|
134
|
-
const base = this.parse(constraintStr.slice(1));
|
|
135
|
-
return this.compare(version, base) < 0;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// Range (1.2.3 - 2.3.4)
|
|
139
|
-
const rangeMatch = constraintStr.match(/^([\d.]+)\s*-\s*([\d.]+)$/);
|
|
140
|
-
if (rangeMatch) {
|
|
141
|
-
const min = this.parse(rangeMatch[1]);
|
|
142
|
-
const max = this.parse(rangeMatch[2]);
|
|
143
|
-
return this.compare(version, min) >= 0 && this.compare(version, max) <= 0;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
return false;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* Determine compatibility level between two versions
|
|
151
|
-
*/
|
|
152
|
-
static getCompatibilityLevel(from: SemanticVersion, to: SemanticVersion): CompatibilityLevel {
|
|
153
|
-
const cmp = this.compare(from, to);
|
|
154
|
-
|
|
155
|
-
// Same version
|
|
156
|
-
if (cmp === 0) {
|
|
157
|
-
return 'fully-compatible';
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// Major version changed - breaking changes
|
|
161
|
-
if (from.major !== to.major) {
|
|
162
|
-
return 'breaking-changes';
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// Minor version increased - backward compatible
|
|
166
|
-
if (from.minor < to.minor) {
|
|
167
|
-
return 'backward-compatible';
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// Patch version increased - fully compatible
|
|
171
|
-
if (from.patch < to.patch) {
|
|
172
|
-
return 'fully-compatible';
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
// Downgrade - incompatible
|
|
176
|
-
return 'incompatible';
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/**
|
|
181
|
-
* Plugin Dependency Resolver
|
|
182
|
-
*
|
|
183
|
-
* Resolves plugin dependencies using topological sorting and conflict detection
|
|
184
|
-
*/
|
|
185
|
-
export class DependencyResolver {
|
|
186
|
-
private logger: ObjectLogger;
|
|
187
|
-
|
|
188
|
-
constructor(logger: ObjectLogger) {
|
|
189
|
-
this.logger = logger.child({ component: 'DependencyResolver' });
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
/**
|
|
193
|
-
* Resolve dependencies using topological sort
|
|
194
|
-
*/
|
|
195
|
-
resolve(
|
|
196
|
-
plugins: Map<string, { version?: string; dependencies?: string[] }>
|
|
197
|
-
): string[] {
|
|
198
|
-
const graph = new Map<string, string[]>();
|
|
199
|
-
const inDegree = new Map<string, number>();
|
|
200
|
-
|
|
201
|
-
// Build dependency graph
|
|
202
|
-
for (const [pluginName, pluginInfo] of plugins) {
|
|
203
|
-
if (!graph.has(pluginName)) {
|
|
204
|
-
graph.set(pluginName, []);
|
|
205
|
-
inDegree.set(pluginName, 0);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
const deps = pluginInfo.dependencies || [];
|
|
209
|
-
for (const dep of deps) {
|
|
210
|
-
// Check if dependency exists
|
|
211
|
-
if (!plugins.has(dep)) {
|
|
212
|
-
throw new Error(`Missing dependency: ${pluginName} requires ${dep}`);
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
// Add edge
|
|
216
|
-
if (!graph.has(dep)) {
|
|
217
|
-
graph.set(dep, []);
|
|
218
|
-
inDegree.set(dep, 0);
|
|
219
|
-
}
|
|
220
|
-
graph.get(dep)!.push(pluginName);
|
|
221
|
-
inDegree.set(pluginName, (inDegree.get(pluginName) || 0) + 1);
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// Topological sort using Kahn's algorithm
|
|
226
|
-
const queue: string[] = [];
|
|
227
|
-
const result: string[] = [];
|
|
228
|
-
|
|
229
|
-
// Add all nodes with no incoming edges
|
|
230
|
-
for (const [node, degree] of inDegree) {
|
|
231
|
-
if (degree === 0) {
|
|
232
|
-
queue.push(node);
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
while (queue.length > 0) {
|
|
237
|
-
const node = queue.shift()!;
|
|
238
|
-
result.push(node);
|
|
239
|
-
|
|
240
|
-
// Reduce in-degree for dependent nodes
|
|
241
|
-
const dependents = graph.get(node) || [];
|
|
242
|
-
for (const dependent of dependents) {
|
|
243
|
-
const newDegree = (inDegree.get(dependent) || 0) - 1;
|
|
244
|
-
inDegree.set(dependent, newDegree);
|
|
245
|
-
|
|
246
|
-
if (newDegree === 0) {
|
|
247
|
-
queue.push(dependent);
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
// Check for circular dependencies
|
|
253
|
-
if (result.length !== plugins.size) {
|
|
254
|
-
const remaining = Array.from(plugins.keys()).filter(p => !result.includes(p));
|
|
255
|
-
this.logger.error('Circular dependency detected', { remaining });
|
|
256
|
-
throw new Error(`Circular dependency detected among: ${remaining.join(', ')}`);
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
this.logger.debug('Dependencies resolved', { order: result });
|
|
260
|
-
return result;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
/**
|
|
264
|
-
* Detect dependency conflicts
|
|
265
|
-
*/
|
|
266
|
-
detectConflicts(
|
|
267
|
-
plugins: Map<string, { version: string; dependencies?: Record<string, VersionConstraint> }>
|
|
268
|
-
): DependencyConflict[] {
|
|
269
|
-
const conflicts: DependencyConflict[] = [];
|
|
270
|
-
const versionRequirements = new Map<string, Map<string, VersionConstraint>>();
|
|
271
|
-
|
|
272
|
-
// Collect all version requirements
|
|
273
|
-
for (const [pluginName, pluginInfo] of plugins) {
|
|
274
|
-
if (!pluginInfo.dependencies) continue;
|
|
275
|
-
|
|
276
|
-
for (const [depName, constraint] of Object.entries(pluginInfo.dependencies)) {
|
|
277
|
-
if (!versionRequirements.has(depName)) {
|
|
278
|
-
versionRequirements.set(depName, new Map());
|
|
279
|
-
}
|
|
280
|
-
versionRequirements.get(depName)!.set(pluginName, constraint);
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
// Check for version mismatches
|
|
285
|
-
for (const [depName, requirements] of versionRequirements) {
|
|
286
|
-
const depInfo = plugins.get(depName);
|
|
287
|
-
if (!depInfo) continue;
|
|
288
|
-
|
|
289
|
-
const depVersion = SemanticVersionManager.parse(depInfo.version);
|
|
290
|
-
const unsatisfied: Array<{ pluginId: string; version: string }> = [];
|
|
291
|
-
|
|
292
|
-
for (const [requiringPlugin, constraint] of requirements) {
|
|
293
|
-
if (!SemanticVersionManager.satisfies(depVersion, constraint)) {
|
|
294
|
-
unsatisfied.push({
|
|
295
|
-
pluginId: requiringPlugin,
|
|
296
|
-
version: constraint as string,
|
|
297
|
-
});
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
if (unsatisfied.length > 0) {
|
|
302
|
-
conflicts.push({
|
|
303
|
-
type: 'version-mismatch',
|
|
304
|
-
severity: 'error',
|
|
305
|
-
description: `Version mismatch for ${depName}: detected ${unsatisfied.length} unsatisfied requirements`,
|
|
306
|
-
plugins: [
|
|
307
|
-
{ pluginId: depName, version: depInfo.version },
|
|
308
|
-
...unsatisfied,
|
|
309
|
-
],
|
|
310
|
-
resolutions: [{
|
|
311
|
-
strategy: 'upgrade',
|
|
312
|
-
description: `Upgrade ${depName} to satisfy all constraints`,
|
|
313
|
-
targetPlugins: [depName],
|
|
314
|
-
automatic: false,
|
|
315
|
-
} as any],
|
|
316
|
-
});
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
// Check for circular dependencies (will be caught by resolve())
|
|
321
|
-
try {
|
|
322
|
-
this.resolve(new Map(
|
|
323
|
-
Array.from(plugins.entries()).map(([name, info]) => [
|
|
324
|
-
name,
|
|
325
|
-
{ version: info.version, dependencies: info.dependencies ? Object.keys(info.dependencies) : [] }
|
|
326
|
-
])
|
|
327
|
-
));
|
|
328
|
-
} catch (error) {
|
|
329
|
-
if (error instanceof Error && error.message.includes('Circular dependency')) {
|
|
330
|
-
conflicts.push({
|
|
331
|
-
type: 'circular-dependency',
|
|
332
|
-
severity: 'critical',
|
|
333
|
-
description: error.message,
|
|
334
|
-
plugins: [], // Would need to extract from error
|
|
335
|
-
resolutions: [{
|
|
336
|
-
strategy: 'manual',
|
|
337
|
-
description: 'Remove circular dependency by restructuring plugins',
|
|
338
|
-
automatic: false,
|
|
339
|
-
} as any],
|
|
340
|
-
});
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
return conflicts;
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
/**
|
|
348
|
-
* Find best version that satisfies all constraints
|
|
349
|
-
*/
|
|
350
|
-
findBestVersion(
|
|
351
|
-
availableVersions: string[],
|
|
352
|
-
constraints: VersionConstraint[]
|
|
353
|
-
): string | undefined {
|
|
354
|
-
// Parse and sort versions (highest first)
|
|
355
|
-
const versions = availableVersions
|
|
356
|
-
.map(v => ({ str: v, parsed: SemanticVersionManager.parse(v) }))
|
|
357
|
-
.sort((a, b) => -SemanticVersionManager.compare(a.parsed, b.parsed));
|
|
358
|
-
|
|
359
|
-
// Find highest version that satisfies all constraints
|
|
360
|
-
for (const version of versions) {
|
|
361
|
-
const satisfiesAll = constraints.every(constraint =>
|
|
362
|
-
SemanticVersionManager.satisfies(version.parsed, constraint)
|
|
363
|
-
);
|
|
364
|
-
|
|
365
|
-
if (satisfiesAll) {
|
|
366
|
-
return version.str;
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
return undefined;
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
/**
|
|
374
|
-
* Check if dependencies form a valid DAG (no cycles)
|
|
375
|
-
*/
|
|
376
|
-
isAcyclic(dependencies: Map<string, string[]>): boolean {
|
|
377
|
-
try {
|
|
378
|
-
const plugins = new Map(
|
|
379
|
-
Array.from(dependencies.entries()).map(([name, deps]) => [
|
|
380
|
-
name,
|
|
381
|
-
{ dependencies: deps }
|
|
382
|
-
])
|
|
383
|
-
);
|
|
384
|
-
this.resolve(plugins);
|
|
385
|
-
return true;
|
|
386
|
-
} catch {
|
|
387
|
-
return false;
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
}
|
|
@@ -1,281 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { createMemoryCache } from './memory-cache';
|
|
3
|
-
import { createMemoryQueue } from './memory-queue';
|
|
4
|
-
import { createMemoryJob } from './memory-job';
|
|
5
|
-
import { createMemoryI18n, resolveLocale } from './memory-i18n';
|
|
6
|
-
import { CORE_FALLBACK_FACTORIES } from './index';
|
|
7
|
-
|
|
8
|
-
describe('CORE_FALLBACK_FACTORIES', () => {
|
|
9
|
-
it('should have exactly 5 entries: metadata, cache, queue, job, i18n', () => {
|
|
10
|
-
expect(Object.keys(CORE_FALLBACK_FACTORIES)).toEqual(['metadata', 'cache', 'queue', 'job', 'i18n']);
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
it('should map to factory functions', () => {
|
|
14
|
-
for (const factory of Object.values(CORE_FALLBACK_FACTORIES)) {
|
|
15
|
-
expect(typeof factory).toBe('function');
|
|
16
|
-
}
|
|
17
|
-
});
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
describe('createMemoryCache', () => {
|
|
21
|
-
it('should return an object with _fallback: true', () => {
|
|
22
|
-
const cache = createMemoryCache();
|
|
23
|
-
expect(cache._fallback).toBe(true);
|
|
24
|
-
expect(cache._serviceName).toBe('cache');
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
it('should set and get a value', async () => {
|
|
28
|
-
const cache = createMemoryCache();
|
|
29
|
-
await cache.set('key1', 'value1');
|
|
30
|
-
expect(await cache.get('key1')).toBe('value1');
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it('should return undefined for missing key', async () => {
|
|
34
|
-
const cache = createMemoryCache();
|
|
35
|
-
expect(await cache.get('nonexistent')).toBeUndefined();
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it('should delete a key', async () => {
|
|
39
|
-
const cache = createMemoryCache();
|
|
40
|
-
await cache.set('key1', 'value1');
|
|
41
|
-
expect(await cache.delete('key1')).toBe(true);
|
|
42
|
-
expect(await cache.get('key1')).toBeUndefined();
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it('should check if a key exists with has()', async () => {
|
|
46
|
-
const cache = createMemoryCache();
|
|
47
|
-
expect(await cache.has('key1')).toBe(false);
|
|
48
|
-
await cache.set('key1', 'value1');
|
|
49
|
-
expect(await cache.has('key1')).toBe(true);
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it('should clear all entries', async () => {
|
|
53
|
-
const cache = createMemoryCache();
|
|
54
|
-
await cache.set('a', 1);
|
|
55
|
-
await cache.set('b', 2);
|
|
56
|
-
await cache.clear();
|
|
57
|
-
expect(await cache.has('a')).toBe(false);
|
|
58
|
-
expect(await cache.has('b')).toBe(false);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it('should expire entries based on TTL', async () => {
|
|
62
|
-
const cache = createMemoryCache();
|
|
63
|
-
// Set with very short TTL (0.001 seconds = 1ms)
|
|
64
|
-
await cache.set('temp', 'data', 0.001);
|
|
65
|
-
// Wait for expiry
|
|
66
|
-
await new Promise(r => setTimeout(r, 20));
|
|
67
|
-
expect(await cache.get('temp')).toBeUndefined();
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it('should track hit/miss stats', async () => {
|
|
71
|
-
const cache = createMemoryCache();
|
|
72
|
-
await cache.set('key1', 'value1');
|
|
73
|
-
await cache.get('key1'); // hit
|
|
74
|
-
await cache.get('missing'); // miss
|
|
75
|
-
const stats = await cache.stats();
|
|
76
|
-
expect(stats.hits).toBe(1);
|
|
77
|
-
expect(stats.misses).toBe(1);
|
|
78
|
-
expect(stats.keyCount).toBe(1);
|
|
79
|
-
});
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
describe('createMemoryQueue', () => {
|
|
83
|
-
it('should return an object with _fallback: true', () => {
|
|
84
|
-
const queue = createMemoryQueue();
|
|
85
|
-
expect(queue._fallback).toBe(true);
|
|
86
|
-
expect(queue._serviceName).toBe('queue');
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
it('should publish and deliver to subscriber synchronously', async () => {
|
|
90
|
-
const queue = createMemoryQueue();
|
|
91
|
-
const received: any[] = [];
|
|
92
|
-
await queue.subscribe('test-q', async (msg: any) => { received.push(msg); });
|
|
93
|
-
const id = await queue.publish('test-q', { hello: 'world' });
|
|
94
|
-
expect(id).toMatch(/^fallback-msg-/);
|
|
95
|
-
expect(received).toHaveLength(1);
|
|
96
|
-
expect(received[0].data).toEqual({ hello: 'world' });
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
it('should not deliver to unsubscribed queue', async () => {
|
|
100
|
-
const queue = createMemoryQueue();
|
|
101
|
-
const received: any[] = [];
|
|
102
|
-
await queue.subscribe('q1', async (msg: any) => { received.push(msg); });
|
|
103
|
-
await queue.unsubscribe('q1');
|
|
104
|
-
await queue.publish('q1', 'data');
|
|
105
|
-
expect(received).toHaveLength(0);
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
it('should return queue size of 0', async () => {
|
|
109
|
-
const queue = createMemoryQueue();
|
|
110
|
-
expect(await queue.getQueueSize()).toBe(0);
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
it('should purge a queue', async () => {
|
|
114
|
-
const queue = createMemoryQueue();
|
|
115
|
-
const received: any[] = [];
|
|
116
|
-
await queue.subscribe('q1', async (msg: any) => { received.push(msg); });
|
|
117
|
-
await queue.purge('q1');
|
|
118
|
-
await queue.publish('q1', 'data');
|
|
119
|
-
expect(received).toHaveLength(0);
|
|
120
|
-
});
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
describe('createMemoryJob', () => {
|
|
124
|
-
it('should return an object with _fallback: true', () => {
|
|
125
|
-
const job = createMemoryJob();
|
|
126
|
-
expect(job._fallback).toBe(true);
|
|
127
|
-
expect(job._serviceName).toBe('job');
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
it('should schedule and list jobs', async () => {
|
|
131
|
-
const job = createMemoryJob();
|
|
132
|
-
await job.schedule('daily-report', '0 0 * * *', async () => {});
|
|
133
|
-
expect(await job.listJobs()).toEqual(['daily-report']);
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
it('should cancel a job', async () => {
|
|
137
|
-
const job = createMemoryJob();
|
|
138
|
-
await job.schedule('temp-job', '* * * * *', async () => {});
|
|
139
|
-
await job.cancel('temp-job');
|
|
140
|
-
expect(await job.listJobs()).toEqual([]);
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
it('should trigger a job handler', async () => {
|
|
144
|
-
const job = createMemoryJob();
|
|
145
|
-
let triggered = false;
|
|
146
|
-
await job.schedule('my-job', '* * * * *', async (ctx: any) => {
|
|
147
|
-
triggered = true;
|
|
148
|
-
expect(ctx.jobId).toBe('my-job');
|
|
149
|
-
expect(ctx.data).toEqual({ key: 'val' });
|
|
150
|
-
});
|
|
151
|
-
await job.trigger('my-job', { key: 'val' });
|
|
152
|
-
expect(triggered).toBe(true);
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
it('should return empty executions', async () => {
|
|
156
|
-
const job = createMemoryJob();
|
|
157
|
-
expect(await job.getExecutions()).toEqual([]);
|
|
158
|
-
});
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
describe('createMemoryI18n', () => {
|
|
162
|
-
it('should return an object with _fallback: true', () => {
|
|
163
|
-
const i18n = createMemoryI18n();
|
|
164
|
-
expect(i18n._fallback).toBe(true);
|
|
165
|
-
expect(i18n._serviceName).toBe('i18n');
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
it('should return the key when no translations are loaded', () => {
|
|
169
|
-
const i18n = createMemoryI18n();
|
|
170
|
-
expect(i18n.t('objects.account.label', 'en')).toBe('objects.account.label');
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
it('should translate after loading translations', () => {
|
|
174
|
-
const i18n = createMemoryI18n();
|
|
175
|
-
i18n.loadTranslations('en', { objects: { account: { label: 'Account' } } });
|
|
176
|
-
expect(i18n.t('objects.account.label', 'en')).toBe('Account');
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
it('should interpolate parameters', () => {
|
|
180
|
-
const i18n = createMemoryI18n();
|
|
181
|
-
i18n.loadTranslations('en', { greeting: 'Hello, {{name}}!' });
|
|
182
|
-
expect(i18n.t('greeting', 'en', { name: 'World' })).toBe('Hello, World!');
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
it('should fall back to default locale', () => {
|
|
186
|
-
const i18n = createMemoryI18n();
|
|
187
|
-
i18n.loadTranslations('en', { hello: 'Hello' });
|
|
188
|
-
// 'fr' has no translations, should fall back to default 'en'
|
|
189
|
-
expect(i18n.t('hello', 'fr')).toBe('Hello');
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
it('should get and set default locale', () => {
|
|
193
|
-
const i18n = createMemoryI18n();
|
|
194
|
-
expect(i18n.getDefaultLocale()).toBe('en');
|
|
195
|
-
i18n.setDefaultLocale('zh-CN');
|
|
196
|
-
expect(i18n.getDefaultLocale()).toBe('zh-CN');
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
it('should list loaded locales', () => {
|
|
200
|
-
const i18n = createMemoryI18n();
|
|
201
|
-
expect(i18n.getLocales()).toEqual([]);
|
|
202
|
-
i18n.loadTranslations('en', { hello: 'Hello' });
|
|
203
|
-
i18n.loadTranslations('zh-CN', { hello: '你好' });
|
|
204
|
-
expect(i18n.getLocales()).toEqual(['en', 'zh-CN']);
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
it('should get all translations for a locale', () => {
|
|
208
|
-
const i18n = createMemoryI18n();
|
|
209
|
-
i18n.loadTranslations('en', { hello: 'Hello', bye: 'Goodbye' });
|
|
210
|
-
expect(i18n.getTranslations('en')).toEqual({ hello: 'Hello', bye: 'Goodbye' });
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
it('should return empty object for unknown locale', () => {
|
|
214
|
-
const i18n = createMemoryI18n();
|
|
215
|
-
expect(i18n.getTranslations('unknown')).toEqual({});
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
it('should merge translations on subsequent loads', () => {
|
|
219
|
-
const i18n = createMemoryI18n();
|
|
220
|
-
i18n.loadTranslations('en', { hello: 'Hello' });
|
|
221
|
-
i18n.loadTranslations('en', { bye: 'Goodbye' });
|
|
222
|
-
expect(i18n.getTranslations('en')).toEqual({ hello: 'Hello', bye: 'Goodbye' });
|
|
223
|
-
});
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
describe('resolveLocale', () => {
|
|
227
|
-
it('should return exact match', () => {
|
|
228
|
-
expect(resolveLocale('zh-CN', ['en', 'zh-CN', 'ja'])).toBe('zh-CN');
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
it('should return case-insensitive match', () => {
|
|
232
|
-
expect(resolveLocale('zh-cn', ['en', 'zh-CN'])).toBe('zh-CN');
|
|
233
|
-
expect(resolveLocale('EN-US', ['en-US', 'zh-CN'])).toBe('en-US');
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
it('should return base language match', () => {
|
|
237
|
-
expect(resolveLocale('zh-TW', ['en', 'zh'])).toBe('zh');
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
it('should return variant expansion (zh → zh-CN)', () => {
|
|
241
|
-
expect(resolveLocale('zh', ['en', 'zh-CN', 'zh-TW'])).toBe('zh-CN');
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
it('should return undefined for no match', () => {
|
|
245
|
-
expect(resolveLocale('fr', ['en', 'zh-CN'])).toBeUndefined();
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
it('should return undefined for empty available locales', () => {
|
|
249
|
-
expect(resolveLocale('en', [])).toBeUndefined();
|
|
250
|
-
});
|
|
251
|
-
|
|
252
|
-
it('should handle es → es-ES expansion', () => {
|
|
253
|
-
expect(resolveLocale('es', ['en', 'es-ES', 'fr'])).toBe('es-ES');
|
|
254
|
-
});
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
describe('createMemoryI18n locale fallback', () => {
|
|
258
|
-
it('should resolve translations via locale fallback (zh → zh-CN)', () => {
|
|
259
|
-
const i18n = createMemoryI18n();
|
|
260
|
-
i18n.loadTranslations('zh-CN', { hello: '你好' });
|
|
261
|
-
expect(i18n.getTranslations('zh')).toEqual({ hello: '你好' });
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
it('should resolve translations via case-insensitive fallback (zh-cn → zh-CN)', () => {
|
|
265
|
-
const i18n = createMemoryI18n();
|
|
266
|
-
i18n.loadTranslations('zh-CN', { hello: '你好' });
|
|
267
|
-
expect(i18n.getTranslations('zh-cn')).toEqual({ hello: '你好' });
|
|
268
|
-
});
|
|
269
|
-
|
|
270
|
-
it('should translate via locale fallback (zh → zh-CN)', () => {
|
|
271
|
-
const i18n = createMemoryI18n();
|
|
272
|
-
i18n.loadTranslations('zh-CN', { greeting: '你好世界' });
|
|
273
|
-
expect(i18n.t('greeting', 'zh')).toBe('你好世界');
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
it('should still fall back to default locale when no locale match at all', () => {
|
|
277
|
-
const i18n = createMemoryI18n();
|
|
278
|
-
i18n.loadTranslations('en', { hello: 'Hello' });
|
|
279
|
-
expect(i18n.t('hello', 'ja')).toBe('Hello');
|
|
280
|
-
});
|
|
281
|
-
});
|
package/src/fallbacks/index.ts
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
-
|
|
3
|
-
import { createMemoryCache } from './memory-cache.js';
|
|
4
|
-
import { createMemoryQueue } from './memory-queue.js';
|
|
5
|
-
import { createMemoryJob } from './memory-job.js';
|
|
6
|
-
import { createMemoryI18n } from './memory-i18n.js';
|
|
7
|
-
import { createMemoryMetadata } from './memory-metadata.js';
|
|
8
|
-
|
|
9
|
-
export { createMemoryCache } from './memory-cache.js';
|
|
10
|
-
export { createMemoryQueue } from './memory-queue.js';
|
|
11
|
-
export { createMemoryJob } from './memory-job.js';
|
|
12
|
-
export { createMemoryI18n, resolveLocale } from './memory-i18n.js';
|
|
13
|
-
export { createMemoryMetadata } from './memory-metadata.js';
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Map of core-criticality service names to their in-memory fallback factories.
|
|
17
|
-
* Used by ObjectKernel.validateSystemRequirements() to auto-inject fallbacks
|
|
18
|
-
* when no real plugin provides the service.
|
|
19
|
-
*/
|
|
20
|
-
export const CORE_FALLBACK_FACTORIES: Record<string, () => Record<string, any>> = {
|
|
21
|
-
metadata: createMemoryMetadata,
|
|
22
|
-
cache: createMemoryCache,
|
|
23
|
-
queue: createMemoryQueue,
|
|
24
|
-
job: createMemoryJob,
|
|
25
|
-
i18n: createMemoryI18n,
|
|
26
|
-
};
|