@objectstack/core 3.0.7 → 3.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,428 @@
1
+ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
+
3
+ import type { ObjectLogger } from './logger.js';
4
+ import { DependencyResolver, SemanticVersionManager } from './dependency-resolver.js';
5
+ import { NamespaceResolver } from './namespace-resolver.js';
6
+
7
+ /**
8
+ * Installed package record in the runtime registry.
9
+ */
10
+ export interface InstalledPackageRecord {
11
+ /** Package identifier */
12
+ packageId: string;
13
+ /** Package version */
14
+ version: string;
15
+ /** Package manifest */
16
+ manifest: Record<string, unknown>;
17
+ /** Installation timestamp */
18
+ installedAt: string;
19
+ /** Current status */
20
+ status: 'installed' | 'disabled' | 'installing' | 'upgrading' | 'uninstalling' | 'error';
21
+ /** Namespaces registered by this package */
22
+ namespaces: string[];
23
+ /** Dependencies of this package */
24
+ dependencies: string[];
25
+ }
26
+
27
+ /**
28
+ * Snapshot of a package's state before upgrade (for rollback).
29
+ */
30
+ export interface PackageSnapshot {
31
+ /** Package identifier */
32
+ packageId: string;
33
+ /** Version before upgrade */
34
+ previousVersion: string;
35
+ /** Full manifest before upgrade */
36
+ previousManifest: Record<string, unknown>;
37
+ /** Namespaces before upgrade */
38
+ previousNamespaces: string[];
39
+ /** Original installation timestamp */
40
+ installedAt: string;
41
+ /** Snapshot timestamp */
42
+ createdAt: string;
43
+ }
44
+
45
+ /**
46
+ * Result of a package installation attempt.
47
+ */
48
+ export interface InstallResult {
49
+ success: boolean;
50
+ packageId: string;
51
+ version: string;
52
+ installedDependencies: string[];
53
+ namespaceConflicts: Array<{ namespace: string; existingPackageId: string }>;
54
+ errorMessage?: string;
55
+ }
56
+
57
+ /**
58
+ * Result of an upgrade attempt.
59
+ */
60
+ export interface UpgradeResult {
61
+ success: boolean;
62
+ packageId: string;
63
+ fromVersion: string;
64
+ toVersion: string;
65
+ snapshot: PackageSnapshot;
66
+ errorMessage?: string;
67
+ }
68
+
69
+ /**
70
+ * Result of a rollback attempt.
71
+ */
72
+ export interface RollbackResult {
73
+ success: boolean;
74
+ packageId: string;
75
+ restoredVersion: string;
76
+ errorMessage?: string;
77
+ }
78
+
79
+ /**
80
+ * Package Manager
81
+ *
82
+ * Runtime implementation for the full package lifecycle:
83
+ * install → upgrade → rollback → uninstall.
84
+ *
85
+ * Consumes the protocol schemas defined in @objectstack/spec:
86
+ * - DependencyResolutionResultSchema
87
+ * - NamespaceConflictErrorSchema
88
+ * - UpgradePlanSchema / UpgradeSnapshotSchema
89
+ * - PackageArtifactSchema
90
+ *
91
+ * Coordinates with:
92
+ * - DependencyResolver for topological ordering and conflict detection
93
+ * - NamespaceResolver for metadata collision prevention
94
+ */
95
+ export class PackageManager {
96
+ private logger: ObjectLogger;
97
+ private packages: Map<string, InstalledPackageRecord> = new Map();
98
+ private snapshots: Map<string, PackageSnapshot> = new Map();
99
+ private dependencyResolver: DependencyResolver;
100
+ private namespaceResolver: NamespaceResolver;
101
+ private platformVersion: string;
102
+
103
+ constructor(
104
+ logger: ObjectLogger,
105
+ options: { platformVersion?: string } = {},
106
+ ) {
107
+ this.logger = logger.child({ component: 'PackageManager' });
108
+ this.dependencyResolver = new DependencyResolver(logger);
109
+ this.namespaceResolver = new NamespaceResolver(logger);
110
+ this.platformVersion = options.platformVersion || '3.0.0';
111
+ }
112
+
113
+ /**
114
+ * Install a package with full dependency resolution and namespace checking.
115
+ */
116
+ async install(
117
+ packageId: string,
118
+ version: string,
119
+ manifest: Record<string, unknown>,
120
+ ): Promise<InstallResult> {
121
+ this.logger.info('Installing package', { packageId, version });
122
+
123
+ // 1. Check if already installed
124
+ if (this.packages.has(packageId)) {
125
+ const existing = this.packages.get(packageId)!;
126
+ if (existing.status === 'installed') {
127
+ return {
128
+ success: false,
129
+ packageId,
130
+ version,
131
+ installedDependencies: [],
132
+ namespaceConflicts: [],
133
+ errorMessage: `Package ${packageId}@${existing.version} is already installed. Use upgrade instead.`,
134
+ };
135
+ }
136
+ }
137
+
138
+ // 2. Check platform compatibility
139
+ const engine = (manifest as any).engine?.objectstack as string | undefined;
140
+ if (engine) {
141
+ const platformSemver = SemanticVersionManager.parse(this.platformVersion);
142
+ if (!SemanticVersionManager.satisfies(platformSemver, engine)) {
143
+ return {
144
+ success: false,
145
+ packageId,
146
+ version,
147
+ installedDependencies: [],
148
+ namespaceConflicts: [],
149
+ errorMessage: `Package requires platform ${engine}, but current platform is v${this.platformVersion}`,
150
+ };
151
+ }
152
+ }
153
+
154
+ // 3. Check namespace conflicts
155
+ const namespaces = this.namespaceResolver.extractNamespaces(manifest);
156
+ const nsCheck = this.namespaceResolver.checkAvailability(packageId, namespaces);
157
+ if (!nsCheck.available) {
158
+ return {
159
+ success: false,
160
+ packageId,
161
+ version,
162
+ installedDependencies: [],
163
+ namespaceConflicts: nsCheck.conflicts.map(c => ({
164
+ namespace: c.namespace,
165
+ existingPackageId: c.existingPackageId,
166
+ })),
167
+ errorMessage: `Namespace conflicts detected: ${nsCheck.conflicts.map(c => c.namespace).join(', ')}`,
168
+ };
169
+ }
170
+
171
+ // 4. Resolve dependencies
172
+ const deps = (manifest as any).dependencies as Record<string, string> | undefined;
173
+ const depNames = deps ? Object.keys(deps) : [];
174
+ const missingDeps = depNames.filter(d => !this.packages.has(d));
175
+ if (missingDeps.length > 0) {
176
+ return {
177
+ success: false,
178
+ packageId,
179
+ version,
180
+ installedDependencies: [],
181
+ namespaceConflicts: [],
182
+ errorMessage: `Missing dependencies: ${missingDeps.join(', ')}`,
183
+ };
184
+ }
185
+
186
+ // 5. Register package
187
+ this.packages.set(packageId, {
188
+ packageId,
189
+ version,
190
+ manifest,
191
+ installedAt: new Date().toISOString(),
192
+ status: 'installed',
193
+ namespaces,
194
+ dependencies: depNames,
195
+ });
196
+
197
+ // 6. Register namespaces
198
+ this.namespaceResolver.register(packageId, namespaces);
199
+
200
+ this.logger.info('Package installed', { packageId, version, namespaces: namespaces.length });
201
+
202
+ return {
203
+ success: true,
204
+ packageId,
205
+ version,
206
+ installedDependencies: depNames,
207
+ namespaceConflicts: [],
208
+ };
209
+ }
210
+
211
+ /**
212
+ * Uninstall a package, checking for dependents first.
213
+ */
214
+ async uninstall(packageId: string): Promise<{ success: boolean; errorMessage?: string }> {
215
+ const pkg = this.packages.get(packageId);
216
+ if (!pkg) {
217
+ return { success: false, errorMessage: `Package ${packageId} is not installed` };
218
+ }
219
+
220
+ // Check if other packages depend on this one
221
+ const dependents: string[] = [];
222
+ for (const [id, record] of this.packages) {
223
+ if (id !== packageId && record.dependencies.includes(packageId)) {
224
+ dependents.push(id);
225
+ }
226
+ }
227
+
228
+ if (dependents.length > 0) {
229
+ return {
230
+ success: false,
231
+ errorMessage: `Cannot uninstall ${packageId}: depended upon by ${dependents.join(', ')}`,
232
+ };
233
+ }
234
+
235
+ // Remove namespaces and package
236
+ this.namespaceResolver.unregister(packageId);
237
+ this.packages.delete(packageId);
238
+ this.snapshots.delete(packageId);
239
+
240
+ this.logger.info('Package uninstalled', { packageId });
241
+ return { success: true };
242
+ }
243
+
244
+ /**
245
+ * Upgrade a package: snapshot → update → register.
246
+ */
247
+ async upgrade(
248
+ packageId: string,
249
+ newVersion: string,
250
+ newManifest: Record<string, unknown>,
251
+ ): Promise<UpgradeResult> {
252
+ const existing = this.packages.get(packageId);
253
+ if (!existing) {
254
+ return {
255
+ success: false,
256
+ packageId,
257
+ fromVersion: '',
258
+ toVersion: newVersion,
259
+ snapshot: { packageId, previousVersion: '', previousManifest: {}, previousNamespaces: [], installedAt: '', createdAt: new Date().toISOString() },
260
+ errorMessage: `Package ${packageId} is not installed`,
261
+ };
262
+ }
263
+
264
+ // 1. Create snapshot for rollback
265
+ const snapshot: PackageSnapshot = {
266
+ packageId,
267
+ previousVersion: existing.version,
268
+ previousManifest: existing.manifest,
269
+ previousNamespaces: [...existing.namespaces],
270
+ installedAt: existing.installedAt,
271
+ createdAt: new Date().toISOString(),
272
+ };
273
+ this.snapshots.set(packageId, snapshot);
274
+
275
+ // 2. Check platform compatibility
276
+ const engine = (newManifest as any).engine?.objectstack as string | undefined;
277
+ if (engine) {
278
+ const platformSemver = SemanticVersionManager.parse(this.platformVersion);
279
+ if (!SemanticVersionManager.satisfies(platformSemver, engine)) {
280
+ return {
281
+ success: false,
282
+ packageId,
283
+ fromVersion: existing.version,
284
+ toVersion: newVersion,
285
+ snapshot,
286
+ errorMessage: `New version requires platform ${engine}, current is v${this.platformVersion}`,
287
+ };
288
+ }
289
+ }
290
+
291
+ // 3. Check namespace changes
292
+ const newNamespaces = this.namespaceResolver.extractNamespaces(newManifest);
293
+ // Temporarily remove old namespaces to check new ones
294
+ this.namespaceResolver.unregister(packageId);
295
+ const nsCheck = this.namespaceResolver.checkAvailability(packageId, newNamespaces);
296
+ if (!nsCheck.available) {
297
+ // Restore old namespaces on failure
298
+ this.namespaceResolver.register(packageId, existing.namespaces);
299
+ return {
300
+ success: false,
301
+ packageId,
302
+ fromVersion: existing.version,
303
+ toVersion: newVersion,
304
+ snapshot,
305
+ errorMessage: `Namespace conflicts in new version: ${nsCheck.conflicts.map(c => c.namespace).join(', ')}`,
306
+ };
307
+ }
308
+
309
+ // 4. Register new namespaces and update record
310
+ this.namespaceResolver.register(packageId, newNamespaces);
311
+ const deps = (newManifest as any).dependencies as Record<string, string> | undefined;
312
+
313
+ this.packages.set(packageId, {
314
+ packageId,
315
+ version: newVersion,
316
+ manifest: newManifest,
317
+ installedAt: existing.installedAt,
318
+ status: 'installed',
319
+ namespaces: newNamespaces,
320
+ dependencies: deps ? Object.keys(deps) : [],
321
+ });
322
+
323
+ this.logger.info('Package upgraded', { packageId, from: existing.version, to: newVersion });
324
+
325
+ return {
326
+ success: true,
327
+ packageId,
328
+ fromVersion: existing.version,
329
+ toVersion: newVersion,
330
+ snapshot,
331
+ };
332
+ }
333
+
334
+ /**
335
+ * Rollback a package to its pre-upgrade snapshot.
336
+ */
337
+ async rollback(packageId: string): Promise<RollbackResult> {
338
+ const snapshot = this.snapshots.get(packageId);
339
+ if (!snapshot) {
340
+ return {
341
+ success: false,
342
+ packageId,
343
+ restoredVersion: '',
344
+ errorMessage: `No upgrade snapshot found for ${packageId}`,
345
+ };
346
+ }
347
+
348
+ // Restore previous state
349
+ this.namespaceResolver.unregister(packageId);
350
+ this.namespaceResolver.register(packageId, snapshot.previousNamespaces);
351
+
352
+ const deps = (snapshot.previousManifest as any).dependencies as Record<string, string> | undefined;
353
+ this.packages.set(packageId, {
354
+ packageId,
355
+ version: snapshot.previousVersion,
356
+ manifest: snapshot.previousManifest,
357
+ installedAt: snapshot.installedAt,
358
+ status: 'installed',
359
+ namespaces: snapshot.previousNamespaces,
360
+ dependencies: deps ? Object.keys(deps) : [],
361
+ });
362
+
363
+ this.snapshots.delete(packageId);
364
+
365
+ this.logger.info('Package rolled back', { packageId, to: snapshot.previousVersion });
366
+
367
+ return {
368
+ success: true,
369
+ packageId,
370
+ restoredVersion: snapshot.previousVersion,
371
+ };
372
+ }
373
+
374
+ /**
375
+ * Get an installed package record.
376
+ */
377
+ getPackage(packageId: string): InstalledPackageRecord | undefined {
378
+ return this.packages.get(packageId);
379
+ }
380
+
381
+ /**
382
+ * List all installed packages.
383
+ */
384
+ listPackages(): InstalledPackageRecord[] {
385
+ return Array.from(this.packages.values());
386
+ }
387
+
388
+ /**
389
+ * Resolve dependencies for a set of packages.
390
+ */
391
+ resolveDependencies(
392
+ packages: Map<string, { version?: string; dependencies?: string[] }>,
393
+ ): string[] {
394
+ return this.dependencyResolver.resolve(packages);
395
+ }
396
+
397
+ /**
398
+ * Check namespace availability for a package's metadata.
399
+ */
400
+ checkNamespaces(packageId: string, config: Record<string, unknown>): {
401
+ available: boolean;
402
+ conflicts: Array<{ namespace: string; existingPackageId: string }>;
403
+ } {
404
+ const namespaces = this.namespaceResolver.extractNamespaces(config);
405
+ const result = this.namespaceResolver.checkAvailability(packageId, namespaces);
406
+ return {
407
+ available: result.available,
408
+ conflicts: result.conflicts.map(c => ({
409
+ namespace: c.namespace,
410
+ existingPackageId: c.existingPackageId,
411
+ })),
412
+ };
413
+ }
414
+
415
+ /**
416
+ * Get the namespace resolver instance.
417
+ */
418
+ getNamespaceResolver(): NamespaceResolver {
419
+ return this.namespaceResolver;
420
+ }
421
+
422
+ /**
423
+ * Get a snapshot for a given package (if available).
424
+ */
425
+ getSnapshot(packageId: string): PackageSnapshot | undefined {
426
+ return this.snapshots.get(packageId);
427
+ }
428
+ }