@contractspec/lib.runtime-sandbox 2.7.6 → 2.7.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.
- package/README.md +42 -22
- package/dist/browser/index.js +364 -120
- package/dist/index.js +364 -120
- package/dist/node/index.js +364 -120
- package/dist/web/index.d.ts +3 -3
- package/package.json +7 -7
- package/src/adapters/pglite/adapter.ts +132 -132
- package/src/index.ts +9 -9
- package/src/ports/database.port.ts +53 -53
- package/src/ports/index.ts +2 -2
- package/src/types/database.types.ts +22 -22
- package/src/web/database/migrations.ts +195 -195
- package/src/web/database/schema.ts +419 -419
- package/src/web/events/local-pubsub.ts +22 -22
- package/src/web/graphql/local-client.ts +495 -496
- package/src/web/index.ts +6 -11
- package/src/web/runtime/seeders/index.ts +611 -352
- package/src/web/runtime/services.ts +104 -105
- package/src/web/storage/indexeddb.ts +98 -98
- package/src/web/utils/id.ts +5 -5
|
@@ -4,26 +4,24 @@
|
|
|
4
4
|
* Uses lazy-loading for PGLite to avoid bundle bloat.
|
|
5
5
|
*/
|
|
6
6
|
import type { DatabasePort } from '@contractspec/lib.runtime-sandbox';
|
|
7
|
-
|
|
7
|
+
import { SANDBOX_MIGRATIONS } from '../database/migrations';
|
|
8
8
|
import { LocalEventBus } from '../events/local-pubsub';
|
|
9
9
|
import { LocalGraphQLClient } from '../graphql/local-client';
|
|
10
10
|
import { LocalStorageService } from '../storage/indexeddb';
|
|
11
11
|
|
|
12
|
-
import { SANDBOX_MIGRATIONS } from '../database/migrations';
|
|
13
|
-
|
|
14
12
|
export type TemplateId = string;
|
|
15
13
|
|
|
16
14
|
export interface LocalRuntimeInitOptions {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
15
|
+
/**
|
|
16
|
+
* Data directory for IndexedDB persistence (optional).
|
|
17
|
+
* If omitted, uses in-memory database.
|
|
18
|
+
*/
|
|
19
|
+
dataDir?: string;
|
|
22
20
|
}
|
|
23
21
|
|
|
24
22
|
export interface TemplateSeedOptions {
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
templateId: TemplateId;
|
|
24
|
+
projectId?: string;
|
|
27
25
|
}
|
|
28
26
|
|
|
29
27
|
const DEFAULT_PROJECT_ID = 'local-project' as const;
|
|
@@ -34,99 +32,100 @@ const DEFAULT_PROJECT_ID = 'local-project' as const;
|
|
|
34
32
|
* Provides lazy-loaded database access via DatabasePort interface.
|
|
35
33
|
*/
|
|
36
34
|
export class LocalRuntimeServices {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
35
|
+
readonly storage = new LocalStorageService();
|
|
36
|
+
readonly pubsub = new LocalEventBus();
|
|
37
|
+
#initialized = false;
|
|
38
|
+
|
|
39
|
+
private _db?: DatabasePort;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Get the database port (must be initialized first).
|
|
43
|
+
*/
|
|
44
|
+
get db(): DatabasePort {
|
|
45
|
+
if (!this._db) {
|
|
46
|
+
throw new Error(
|
|
47
|
+
'LocalRuntimeServices not initialized. Call init() first.'
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
return this._db;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
private _graphql?: LocalGraphQLClient;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Get the GraphQL client (must be initialized first).
|
|
57
|
+
*/
|
|
58
|
+
get graphql(): LocalGraphQLClient {
|
|
59
|
+
if (!this._graphql) {
|
|
60
|
+
throw new Error(
|
|
61
|
+
'LocalRuntimeServices not initialized. Call init() first.'
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
return this._graphql;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Initialize the runtime services.
|
|
69
|
+
*
|
|
70
|
+
* Lazy-loads PGLite adapter to avoid bundle bloat.
|
|
71
|
+
*/
|
|
72
|
+
async init(options: LocalRuntimeInitOptions = {}): Promise<void> {
|
|
73
|
+
if (this.#initialized) return;
|
|
74
|
+
|
|
75
|
+
// Lazy-load PGLite adapter
|
|
76
|
+
const { createPGLiteAdapter } = await import(
|
|
77
|
+
'@contractspec/lib.runtime-sandbox'
|
|
78
|
+
);
|
|
79
|
+
this._db = await createPGLiteAdapter();
|
|
80
|
+
await this._db.init({ dataDir: options.dataDir });
|
|
81
|
+
|
|
82
|
+
// Run migrations
|
|
83
|
+
await this._db.migrate(SANDBOX_MIGRATIONS);
|
|
84
|
+
|
|
85
|
+
// Initialize storage
|
|
86
|
+
await this.storage.init();
|
|
87
|
+
|
|
88
|
+
// Initialize GraphQL client with the new database port
|
|
89
|
+
this._graphql = new LocalGraphQLClient({
|
|
90
|
+
db: this._db,
|
|
91
|
+
storage: this.storage,
|
|
92
|
+
pubsub: this.pubsub,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
this.#initialized = true;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Check if runtime is initialized.
|
|
100
|
+
*/
|
|
101
|
+
isInitialized(): boolean {
|
|
102
|
+
return this.#initialized;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Seed the database with deterministic defaults for a template.
|
|
107
|
+
*
|
|
108
|
+
* - No randomness
|
|
109
|
+
* - No wall-clock timestamps
|
|
110
|
+
* - Unknown templates are a no-op (safe default)
|
|
111
|
+
*/
|
|
112
|
+
async seedTemplate(options: TemplateSeedOptions): Promise<void> {
|
|
113
|
+
if (!this.#initialized) {
|
|
114
|
+
throw new Error('Call init() before seeding templates.');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const projectId = options.projectId ?? DEFAULT_PROJECT_ID;
|
|
118
|
+
|
|
119
|
+
if (!this._db) {
|
|
120
|
+
throw new Error('Database not initialized');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Lazy-load seeders to avoid bundle bloat
|
|
124
|
+
const { seedTemplate } = await import('./seeders');
|
|
125
|
+
await seedTemplate({
|
|
126
|
+
templateId: options.templateId,
|
|
127
|
+
projectId,
|
|
128
|
+
db: this._db,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
132
131
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export interface LocalStorageOptions {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
dbName?: string;
|
|
3
|
+
storeName?: string;
|
|
4
|
+
version?: number;
|
|
5
5
|
}
|
|
6
6
|
|
|
7
7
|
const DEFAULT_DB_NAME = 'contractspec-runtime';
|
|
@@ -9,108 +9,108 @@ const DEFAULT_STORE = 'kv';
|
|
|
9
9
|
const FALLBACK_STORE = new Map<string, unknown>();
|
|
10
10
|
|
|
11
11
|
export class LocalStorageService {
|
|
12
|
-
|
|
12
|
+
private dbPromise?: Promise<IDBDatabase | null>;
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
constructor(private readonly options: LocalStorageOptions = {}) {}
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
async init(): Promise<void> {
|
|
17
|
+
await this.getDb();
|
|
18
|
+
}
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
20
|
+
async get<TValue = unknown>(
|
|
21
|
+
key: string,
|
|
22
|
+
fallback?: TValue
|
|
23
|
+
): Promise<TValue | undefined> {
|
|
24
|
+
const db = await this.getDb();
|
|
25
|
+
if (!db) {
|
|
26
|
+
return (FALLBACK_STORE.get(key) as TValue | undefined) ?? fallback;
|
|
27
|
+
}
|
|
28
|
+
return new Promise((resolve, reject) => {
|
|
29
|
+
const tx = db.transaction(this.storeName, 'readonly');
|
|
30
|
+
const store = tx.objectStore(this.storeName);
|
|
31
|
+
const request = store.get(key);
|
|
32
|
+
request.onsuccess = () => {
|
|
33
|
+
resolve((request.result as TValue | undefined) ?? fallback);
|
|
34
|
+
};
|
|
35
|
+
request.onerror = () => reject(request.error);
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
39
|
+
async set<TValue = unknown>(key: string, value: TValue): Promise<void> {
|
|
40
|
+
const db = await this.getDb();
|
|
41
|
+
if (!db) {
|
|
42
|
+
FALLBACK_STORE.set(key, value);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
await new Promise<void>((resolve, reject) => {
|
|
46
|
+
const tx = db.transaction(this.storeName, 'readwrite');
|
|
47
|
+
const store = tx.objectStore(this.storeName);
|
|
48
|
+
const request = store.put(value, key);
|
|
49
|
+
request.onsuccess = () => resolve();
|
|
50
|
+
request.onerror = () => reject(request.error);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
53
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
54
|
+
async delete(key: string): Promise<void> {
|
|
55
|
+
const db = await this.getDb();
|
|
56
|
+
if (!db) {
|
|
57
|
+
FALLBACK_STORE.delete(key);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
await new Promise<void>((resolve, reject) => {
|
|
61
|
+
const tx = db.transaction(this.storeName, 'readwrite');
|
|
62
|
+
const store = tx.objectStore(this.storeName);
|
|
63
|
+
const request = store.delete(key);
|
|
64
|
+
request.onsuccess = () => resolve();
|
|
65
|
+
request.onerror = () => reject(request.error);
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
68
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
69
|
+
async clear(): Promise<void> {
|
|
70
|
+
const db = await this.getDb();
|
|
71
|
+
if (!db) {
|
|
72
|
+
FALLBACK_STORE.clear();
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
await new Promise<void>((resolve, reject) => {
|
|
76
|
+
const tx = db.transaction(this.storeName, 'readwrite');
|
|
77
|
+
const store = tx.objectStore(this.storeName);
|
|
78
|
+
const request = store.clear();
|
|
79
|
+
request.onsuccess = () => resolve();
|
|
80
|
+
request.onerror = () => reject(request.error);
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
83
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
84
|
+
private get storeName() {
|
|
85
|
+
return this.options.storeName ?? DEFAULT_STORE;
|
|
86
|
+
}
|
|
87
87
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
88
|
+
private async getDb(): Promise<IDBDatabase | null> {
|
|
89
|
+
if (typeof indexedDB === 'undefined') {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
if (!this.dbPromise) {
|
|
93
|
+
this.dbPromise = this.openDb();
|
|
94
|
+
}
|
|
95
|
+
return this.dbPromise;
|
|
96
|
+
}
|
|
97
97
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
98
|
+
private openDb(): Promise<IDBDatabase> {
|
|
99
|
+
return new Promise((resolve, reject) => {
|
|
100
|
+
const request = indexedDB.open(
|
|
101
|
+
this.options.dbName ?? DEFAULT_DB_NAME,
|
|
102
|
+
this.options.version ?? 1
|
|
103
|
+
);
|
|
104
|
+
request.onerror = () => reject(request.error);
|
|
105
|
+
request.onsuccess = () => {
|
|
106
|
+
resolve(request.result);
|
|
107
|
+
};
|
|
108
|
+
request.onupgradeneeded = () => {
|
|
109
|
+
const db = request.result;
|
|
110
|
+
if (!db.objectStoreNames.contains(this.storeName)) {
|
|
111
|
+
db.createObjectStore(this.storeName);
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
116
|
}
|
package/src/web/utils/id.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export function generateId(prefix?: string): string {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
const base =
|
|
3
|
+
typeof crypto !== 'undefined' && 'randomUUID' in crypto
|
|
4
|
+
? crypto.randomUUID()
|
|
5
|
+
: Math.random().toString(36).slice(2, 10);
|
|
6
|
+
return prefix ? `${prefix}_${base}` : base;
|
|
7
7
|
}
|