@stream44.studio/encapsulate 0.4.0-rc.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/.dco-signatures +9 -0
- package/.github/workflows/dco.yaml +12 -0
- package/.github/workflows/test.yml +26 -0
- package/.o/GordianOpenIntegrity-CurrentLifehash.svg +1026 -0
- package/.o/GordianOpenIntegrity-InceptionLifehash.svg +1026 -0
- package/.o/GordianOpenIntegrity.yaml +25 -0
- package/.o/assets/Hero-Explosion-v0.jpeg +0 -0
- package/DCO.md +34 -0
- package/LICENSE.md +8 -0
- package/README.md +46 -0
- package/package.json +33 -0
- package/src/capsule-projectors/CapsuleModuleProjector.v0.ts +1725 -0
- package/src/encapsulate.ts +881 -0
- package/src/spine-contracts/CapsuleSpineContract.v0/Membrane.v0.ts +705 -0
- package/src/spine-contracts/CapsuleSpineContract.v0/README.md +28 -0
- package/src/spine-contracts/CapsuleSpineContract.v0/Static.v0.ts +395 -0
- package/src/spine-factories/CapsuleSpineFactory.v0.ts +582 -0
- package/src/spine-factories/TimingObserver.ts +26 -0
- package/src/static-analyzer.v0.ts +1898 -0
- package/structs/Capsule.ts +22 -0
- package/tsconfig.json +30 -0
|
@@ -0,0 +1,582 @@
|
|
|
1
|
+
import { join, dirname, resolve as pathResolve } from 'path'
|
|
2
|
+
import { writeFile, mkdir, readFile, stat } from 'fs/promises'
|
|
3
|
+
import { Spine, SpineRuntime, CapsulePropertyTypes, makeImportStack, merge } from "../encapsulate"
|
|
4
|
+
import { StaticAnalyzer } from "../../src/static-analyzer.v0"
|
|
5
|
+
import { CapsuleModuleProjector } from "../../src/capsule-projectors/CapsuleModuleProjector.v0"
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
export { merge }
|
|
9
|
+
|
|
10
|
+
// Pure filesystem resolve — never calls Bun.resolve or createRequire to avoid
|
|
11
|
+
// Bun's internal module resolver which can OOM on large capsule trees.
|
|
12
|
+
//
|
|
13
|
+
// Resolution strategy:
|
|
14
|
+
// 1. Scoped packages: @scope/package/path → spineRoot/scope/packages/package/path.ts
|
|
15
|
+
// (matches tsconfig.paths.json pattern used across the workspace)
|
|
16
|
+
// 2. Scoped package root: @scope/package → resolve via package.json exports
|
|
17
|
+
// 3. Relative paths are resolved by the caller before reaching this function.
|
|
18
|
+
async function resolve(uri: string, fromPath: string, spineRoot?: string): Promise<string> {
|
|
19
|
+
|
|
20
|
+
// Scoped package: @scope/package/subpath
|
|
21
|
+
if (uri.startsWith('@') && spineRoot) {
|
|
22
|
+
const match = uri.match(/^@([^/]+)\/([^/]+)(?:\/(.+))?$/)
|
|
23
|
+
if (match) {
|
|
24
|
+
const [, scope, pkg, subpath] = match
|
|
25
|
+
|
|
26
|
+
if (subpath) {
|
|
27
|
+
// @scope/package/path → spineRoot/scope/packages/package/path.ts
|
|
28
|
+
const fsPath = join(spineRoot, scope, 'packages', pkg, subpath + '.ts')
|
|
29
|
+
try {
|
|
30
|
+
await stat(fsPath)
|
|
31
|
+
return fsPath
|
|
32
|
+
} catch {
|
|
33
|
+
// Try without .ts extension (already has extension)
|
|
34
|
+
try {
|
|
35
|
+
await stat(join(spineRoot, scope, 'packages', pkg, subpath))
|
|
36
|
+
return join(spineRoot, scope, 'packages', pkg, subpath)
|
|
37
|
+
} catch {
|
|
38
|
+
// Fall through to package.json exports resolution
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Try resolving via package.json exports
|
|
44
|
+
const packageDir = join(spineRoot, scope, 'packages', pkg)
|
|
45
|
+
try {
|
|
46
|
+
const packageJsonPath = join(packageDir, 'package.json')
|
|
47
|
+
const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf-8'))
|
|
48
|
+
|
|
49
|
+
if (subpath && packageJson.exports) {
|
|
50
|
+
// Look for matching export: "./" + subpath or "./" + subpath + ".ts"
|
|
51
|
+
const exportKey = './' + subpath
|
|
52
|
+
const exportValue = packageJson.exports[exportKey]
|
|
53
|
+
if (typeof exportValue === 'string') {
|
|
54
|
+
return pathResolve(packageDir, exportValue)
|
|
55
|
+
}
|
|
56
|
+
} else if (!subpath && packageJson.exports?.['.']) {
|
|
57
|
+
const mainExport = packageJson.exports['.']
|
|
58
|
+
if (typeof mainExport === 'string') {
|
|
59
|
+
return pathResolve(packageDir, mainExport)
|
|
60
|
+
}
|
|
61
|
+
} else if (!subpath && packageJson.main) {
|
|
62
|
+
return pathResolve(packageDir, packageJson.main)
|
|
63
|
+
}
|
|
64
|
+
} catch {
|
|
65
|
+
// package.json doesn't exist or is invalid
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Traverse up from spineRoot looking for the package:
|
|
69
|
+
// 1. Check if current dir IS the package (self-package)
|
|
70
|
+
// 2. Check node_modules/@scope/pkg at each level
|
|
71
|
+
let dir = spineRoot
|
|
72
|
+
while (true) {
|
|
73
|
+
// Check if this directory's package.json matches the requested package
|
|
74
|
+
try {
|
|
75
|
+
const pjPath = join(dir, 'package.json')
|
|
76
|
+
const pj = JSON.parse(await readFile(pjPath, 'utf-8'))
|
|
77
|
+
if (pj.name === `@${scope}/${pkg}`) {
|
|
78
|
+
if (subpath) {
|
|
79
|
+
if (pj.exports) {
|
|
80
|
+
const exportKey = './' + subpath
|
|
81
|
+
const exportValue = pj.exports[exportKey]
|
|
82
|
+
if (typeof exportValue === 'string') {
|
|
83
|
+
return pathResolve(dir, exportValue)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const fsPath = join(dir, subpath + '.ts')
|
|
87
|
+
try { await stat(fsPath); return fsPath } catch { }
|
|
88
|
+
try { await stat(join(dir, subpath)); return join(dir, subpath) } catch { }
|
|
89
|
+
} else if (pj.exports?.['.']) {
|
|
90
|
+
const mainExport = pj.exports['.']
|
|
91
|
+
if (typeof mainExport === 'string') {
|
|
92
|
+
return pathResolve(dir, mainExport)
|
|
93
|
+
}
|
|
94
|
+
} else if (pj.main) {
|
|
95
|
+
return pathResolve(dir, pj.main)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
} catch { }
|
|
99
|
+
|
|
100
|
+
// Check node_modules/@scope/pkg
|
|
101
|
+
const nmCandidate = join(dir, 'node_modules', `@${scope}`, pkg)
|
|
102
|
+
try {
|
|
103
|
+
const nmPjPath = join(nmCandidate, 'package.json')
|
|
104
|
+
const nmPj = JSON.parse(await readFile(nmPjPath, 'utf-8'))
|
|
105
|
+
if (subpath) {
|
|
106
|
+
if (nmPj.exports) {
|
|
107
|
+
const exportKey = './' + subpath
|
|
108
|
+
const exportValue = nmPj.exports[exportKey]
|
|
109
|
+
if (typeof exportValue === 'string') {
|
|
110
|
+
return pathResolve(nmCandidate, exportValue)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const fsPath = join(nmCandidate, subpath + '.ts')
|
|
114
|
+
try { await stat(fsPath); return fsPath } catch { }
|
|
115
|
+
try { await stat(join(nmCandidate, subpath)); return join(nmCandidate, subpath) } catch { }
|
|
116
|
+
} else if (nmPj.exports?.['.']) {
|
|
117
|
+
const mainExport = nmPj.exports['.']
|
|
118
|
+
if (typeof mainExport === 'string') {
|
|
119
|
+
return pathResolve(nmCandidate, mainExport)
|
|
120
|
+
}
|
|
121
|
+
} else if (nmPj.main) {
|
|
122
|
+
return pathResolve(nmCandidate, nmPj.main)
|
|
123
|
+
}
|
|
124
|
+
} catch { }
|
|
125
|
+
|
|
126
|
+
const parent = dirname(dir)
|
|
127
|
+
if (parent === dir) break
|
|
128
|
+
dir = parent
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Also traverse up from fromPath (the importing file) checking:
|
|
132
|
+
// 1. If current dir IS the package (self-package resolution)
|
|
133
|
+
// 2. node_modules/@scope/pkg at each level
|
|
134
|
+
let fromDir = dirname(fromPath)
|
|
135
|
+
while (true) {
|
|
136
|
+
// Check if this directory's package.json matches the requested package
|
|
137
|
+
try {
|
|
138
|
+
const pjPath = join(fromDir, 'package.json')
|
|
139
|
+
const pj = JSON.parse(await readFile(pjPath, 'utf-8'))
|
|
140
|
+
if (pj.name === `@${scope}/${pkg}`) {
|
|
141
|
+
if (subpath) {
|
|
142
|
+
if (pj.exports) {
|
|
143
|
+
const exportKey = './' + subpath
|
|
144
|
+
const exportValue = pj.exports[exportKey]
|
|
145
|
+
if (typeof exportValue === 'string') {
|
|
146
|
+
return pathResolve(fromDir, exportValue)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
const fsPath = join(fromDir, subpath + '.ts')
|
|
150
|
+
try { await stat(fsPath); return fsPath } catch { }
|
|
151
|
+
try { await stat(join(fromDir, subpath)); return join(fromDir, subpath) } catch { }
|
|
152
|
+
} else if (pj.exports?.['.']) {
|
|
153
|
+
const mainExport = pj.exports['.']
|
|
154
|
+
if (typeof mainExport === 'string') {
|
|
155
|
+
return pathResolve(fromDir, mainExport)
|
|
156
|
+
}
|
|
157
|
+
} else if (pj.main) {
|
|
158
|
+
return pathResolve(fromDir, pj.main)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
} catch { }
|
|
162
|
+
|
|
163
|
+
const nmCandidate = join(fromDir, 'node_modules', `@${scope}`, pkg)
|
|
164
|
+
try {
|
|
165
|
+
const nmPjPath = join(nmCandidate, 'package.json')
|
|
166
|
+
const nmPj = JSON.parse(await readFile(nmPjPath, 'utf-8'))
|
|
167
|
+
if (subpath) {
|
|
168
|
+
if (nmPj.exports) {
|
|
169
|
+
const exportKey = './' + subpath
|
|
170
|
+
const exportValue = nmPj.exports[exportKey]
|
|
171
|
+
if (typeof exportValue === 'string') {
|
|
172
|
+
return pathResolve(nmCandidate, exportValue)
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
const fsPath = join(nmCandidate, subpath + '.ts')
|
|
176
|
+
try { await stat(fsPath); return fsPath } catch { }
|
|
177
|
+
try { await stat(join(nmCandidate, subpath)); return join(nmCandidate, subpath) } catch { }
|
|
178
|
+
} else if (nmPj.exports?.['.']) {
|
|
179
|
+
const mainExport = nmPj.exports['.']
|
|
180
|
+
if (typeof mainExport === 'string') {
|
|
181
|
+
return pathResolve(nmCandidate, mainExport)
|
|
182
|
+
}
|
|
183
|
+
} else if (nmPj.main) {
|
|
184
|
+
return pathResolve(nmCandidate, nmPj.main)
|
|
185
|
+
}
|
|
186
|
+
} catch { }
|
|
187
|
+
|
|
188
|
+
const parent = dirname(fromDir)
|
|
189
|
+
if (parent === fromDir) break
|
|
190
|
+
fromDir = parent
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Absolute path — probe extensions
|
|
196
|
+
if (uri.startsWith('/')) {
|
|
197
|
+
// Try exact path first
|
|
198
|
+
try {
|
|
199
|
+
const s = await stat(uri)
|
|
200
|
+
if (s.isFile()) return uri
|
|
201
|
+
if (s.isDirectory()) {
|
|
202
|
+
for (const idx of ['index.ts', 'index.js']) {
|
|
203
|
+
try { await stat(join(uri, idx)); return join(uri, idx) } catch { }
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
} catch { }
|
|
207
|
+
// Try with extensions
|
|
208
|
+
for (const ext of ['.ts', '.js', '.mjs']) {
|
|
209
|
+
try { await stat(uri + ext); return uri + ext } catch { }
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Non-scoped bare specifier — try node_modules resolution from fromPath
|
|
214
|
+
if (!uri.startsWith('.') && !uri.startsWith('/')) {
|
|
215
|
+
// Walk up from fromPath looking for node_modules
|
|
216
|
+
let dir = dirname(fromPath)
|
|
217
|
+
while (true) {
|
|
218
|
+
const candidate = join(dir, 'node_modules', uri)
|
|
219
|
+
try {
|
|
220
|
+
const s = await stat(candidate)
|
|
221
|
+
if (s.isDirectory()) {
|
|
222
|
+
// Try package.json main/exports
|
|
223
|
+
try {
|
|
224
|
+
const pj = JSON.parse(await readFile(join(candidate, 'package.json'), 'utf-8'))
|
|
225
|
+
if (pj.main) return pathResolve(candidate, pj.main)
|
|
226
|
+
} catch { }
|
|
227
|
+
return join(candidate, 'index.js')
|
|
228
|
+
}
|
|
229
|
+
return candidate
|
|
230
|
+
} catch { }
|
|
231
|
+
// Try with extensions
|
|
232
|
+
for (const ext of ['.ts', '.js', '.mjs']) {
|
|
233
|
+
try {
|
|
234
|
+
await stat(candidate + ext)
|
|
235
|
+
return candidate + ext
|
|
236
|
+
} catch { }
|
|
237
|
+
}
|
|
238
|
+
const parent = dirname(dir)
|
|
239
|
+
if (parent === dir) break
|
|
240
|
+
dir = parent
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
throw new Error(`[encapsulate resolve] Cannot resolve '${uri}' from '${fromPath}' (spineRoot: ${spineRoot})`)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
export async function CapsuleSpineFactory({
|
|
249
|
+
spineFilesystemRoot,
|
|
250
|
+
capsuleModuleProjectionRoot,
|
|
251
|
+
capsuleModuleProjectionPackage,
|
|
252
|
+
staticAnalysisEnabled = true,
|
|
253
|
+
onMembraneEvent,
|
|
254
|
+
enableCallerStackInference = false,
|
|
255
|
+
spineContracts,
|
|
256
|
+
timing: timingParam
|
|
257
|
+
}: {
|
|
258
|
+
spineFilesystemRoot: string,
|
|
259
|
+
capsuleModuleProjectionRoot?: string,
|
|
260
|
+
capsuleModuleProjectionPackage?: string,
|
|
261
|
+
staticAnalysisEnabled?: boolean,
|
|
262
|
+
onMembraneEvent?: (event: any) => void,
|
|
263
|
+
enableCallerStackInference?: boolean,
|
|
264
|
+
spineContracts: Record<string, any>,
|
|
265
|
+
timing?: { record: (step: string) => void, recordMajor: (step: string) => void, chalk?: any }
|
|
266
|
+
}) {
|
|
267
|
+
|
|
268
|
+
if (capsuleModuleProjectionRoot) capsuleModuleProjectionRoot = capsuleModuleProjectionRoot.replace(/^file:\/\//, '')
|
|
269
|
+
if (spineFilesystemRoot) spineFilesystemRoot = spineFilesystemRoot.replace(/^file:\/\//, '')
|
|
270
|
+
|
|
271
|
+
const timing = timingParam
|
|
272
|
+
|
|
273
|
+
timing?.recordMajor('CAPSULE SPINE FACTORY: INITIALIZATION')
|
|
274
|
+
|
|
275
|
+
const SingletonRegistry = () => {
|
|
276
|
+
const registry = new Map<string, Promise<any>>()
|
|
277
|
+
|
|
278
|
+
return {
|
|
279
|
+
async ensure(id: string, createHandler: () => Promise<any>) {
|
|
280
|
+
if (!registry.has(id)) {
|
|
281
|
+
registry.set(id, createHandler())
|
|
282
|
+
}
|
|
283
|
+
return registry.get(id)!
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const registry = SingletonRegistry()
|
|
289
|
+
|
|
290
|
+
const spineContractInstances: {
|
|
291
|
+
encapsulation: Record<string, any>,
|
|
292
|
+
runtime: Record<string, any>
|
|
293
|
+
} = {
|
|
294
|
+
encapsulation: {},
|
|
295
|
+
runtime: {}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const sourceSpine: { encapsulate?: any } = {}
|
|
299
|
+
const commonSpineContractOpts = {
|
|
300
|
+
spineFilesystemRoot,
|
|
301
|
+
resolve: async (uri: string, parentFilepath: string) => {
|
|
302
|
+
// For relative paths, join with parent directory first
|
|
303
|
+
if (/^\.\.?\//.test(uri)) {
|
|
304
|
+
return await resolve(join(parentFilepath, '..', uri), spineFilesystemRoot, spineFilesystemRoot)
|
|
305
|
+
}
|
|
306
|
+
// For absolute/package paths, use custom resolve with spine root
|
|
307
|
+
return await resolve(uri, parentFilepath, spineFilesystemRoot)
|
|
308
|
+
},
|
|
309
|
+
importCapsule: (() => {
|
|
310
|
+
const callSequence: string[] = []
|
|
311
|
+
const MAX_SEQUENCE = 300
|
|
312
|
+
const REPEAT_THRESHOLD = 3
|
|
313
|
+
|
|
314
|
+
function detectRepeatingSequence(seq: string[]): string[] | null {
|
|
315
|
+
const len = seq.length
|
|
316
|
+
// Check subsequences of length 2..len/3
|
|
317
|
+
for (let subLen = 2; subLen <= Math.floor(len / REPEAT_THRESHOLD); subLen++) {
|
|
318
|
+
const tail = seq.slice(len - subLen)
|
|
319
|
+
let repeats = 1
|
|
320
|
+
for (let offset = subLen; offset <= len - subLen; offset += subLen) {
|
|
321
|
+
const chunk = seq.slice(len - subLen - offset, len - offset)
|
|
322
|
+
if (chunk.length === tail.length && chunk.every((v, i) => v === tail[i])) {
|
|
323
|
+
repeats++
|
|
324
|
+
} else {
|
|
325
|
+
break
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
if (repeats >= REPEAT_THRESHOLD) {
|
|
329
|
+
return tail
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return null
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
let callCount = 0
|
|
336
|
+
|
|
337
|
+
return async (filepath: string) => {
|
|
338
|
+
const shortPath = filepath.replace(/^.*\/genesis\//, '')
|
|
339
|
+
|
|
340
|
+
callCount++
|
|
341
|
+
callSequence.push(shortPath)
|
|
342
|
+
if (callSequence.length > MAX_SEQUENCE) {
|
|
343
|
+
callSequence.splice(0, callSequence.length - MAX_SEQUENCE)
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const repeating = detectRepeatingSequence(callSequence)
|
|
347
|
+
if (repeating) {
|
|
348
|
+
throw new Error(
|
|
349
|
+
`Circular capsule loading detected! The following sequence repeated ${REPEAT_THRESHOLD} times:\n` +
|
|
350
|
+
repeating.map((p, i) => ` ${i + 1}. ${p}`).join('\n')
|
|
351
|
+
)
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
timing?.record(`importCapsule: Called for ${shortPath}`)
|
|
355
|
+
const result = await registry.ensure(filepath, async () => {
|
|
356
|
+
timing?.recordMajor(`importCapsule: Starting import for ${shortPath}`)
|
|
357
|
+
const importStart = Date.now()
|
|
358
|
+
const exports = await import(filepath)
|
|
359
|
+
const importDuration = Date.now() - importStart
|
|
360
|
+
timing?.recordMajor(`importCapsule: import() took ${importDuration}ms for ${shortPath}`)
|
|
361
|
+
|
|
362
|
+
if (importDuration > 10) {
|
|
363
|
+
if (timing) {
|
|
364
|
+
console.log(timing.chalk.red(`\n⚠️ WARNING: Slow module load detected!`))
|
|
365
|
+
console.log(timing.chalk.red(` Module: ${filepath}`))
|
|
366
|
+
console.log(timing.chalk.red(` Load time: ${importDuration}ms`))
|
|
367
|
+
console.log(timing.chalk.red(` Consider using dynamic imports to load heavy dependencies only when needed.\n`))
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (typeof exports.capsule !== 'function') throw new Error(`Module at '${filepath}' does not export 'capsule'!`)
|
|
372
|
+
|
|
373
|
+
const capsuleStart = Date.now()
|
|
374
|
+
const capsule = await exports.capsule({
|
|
375
|
+
encapsulate: sourceSpine.encapsulate,
|
|
376
|
+
CapsulePropertyTypes,
|
|
377
|
+
makeImportStack
|
|
378
|
+
})
|
|
379
|
+
const capsuleDuration = Date.now() - capsuleStart
|
|
380
|
+
timing?.recordMajor(`importCapsule: exports.capsule() took ${capsuleDuration}ms for ${shortPath}`)
|
|
381
|
+
|
|
382
|
+
timing?.record(`importCapsule: Returning result for ${shortPath}`)
|
|
383
|
+
|
|
384
|
+
return capsule
|
|
385
|
+
})
|
|
386
|
+
return result
|
|
387
|
+
}
|
|
388
|
+
})(),
|
|
389
|
+
encapsulateOpts: {
|
|
390
|
+
CapsulePropertyTypes
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
timing?.recordMajor('SPINE CONTRACTS: INITIALIZATION')
|
|
395
|
+
|
|
396
|
+
for (const spineContractUri in spineContracts) {
|
|
397
|
+
spineContractInstances.encapsulation[spineContractUri] = spineContracts[spineContractUri]({
|
|
398
|
+
...commonSpineContractOpts,
|
|
399
|
+
freezeCapsule: async ({ spineContractUri, capsule }: { spineContractUri: string, capsule: any }): Promise<any> => {
|
|
400
|
+
|
|
401
|
+
if (!projector) {
|
|
402
|
+
throw new Error('capsuleModuleProjectionRoot must be provided to enable freezing')
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
let snapshotValues = {}
|
|
406
|
+
|
|
407
|
+
// Create a new set per freezeCapsule call to track circular dependencies within this projection tree
|
|
408
|
+
const projectingCapsules = new Set<string>()
|
|
409
|
+
|
|
410
|
+
const projected = await projector.projectCapsule({
|
|
411
|
+
capsule,
|
|
412
|
+
capsules,
|
|
413
|
+
snapshotValues,
|
|
414
|
+
spineContractUri,
|
|
415
|
+
projectingCapsules
|
|
416
|
+
})
|
|
417
|
+
|
|
418
|
+
return snapshotValues
|
|
419
|
+
}
|
|
420
|
+
})
|
|
421
|
+
spineContractInstances.runtime[spineContractUri] = spineContracts[spineContractUri]({
|
|
422
|
+
...commonSpineContractOpts,
|
|
423
|
+
enableCallerStackInference,
|
|
424
|
+
onMembraneEvent,
|
|
425
|
+
})
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
timing?.recordMajor('CAPSULE MODULE PROJECTOR: INITIALIZATION')
|
|
429
|
+
|
|
430
|
+
const projector = capsuleModuleProjectionRoot ? CapsuleModuleProjector({
|
|
431
|
+
spineStore: {
|
|
432
|
+
writeFile: async (filepath: string, content: string) => {
|
|
433
|
+
filepath = join(spineFilesystemRoot, filepath)
|
|
434
|
+
await mkdir(dirname(filepath), { recursive: true })
|
|
435
|
+
await writeFile(filepath, content, 'utf-8')
|
|
436
|
+
},
|
|
437
|
+
getStats: async (filepath: string) => {
|
|
438
|
+
filepath = join(spineFilesystemRoot, filepath)
|
|
439
|
+
try {
|
|
440
|
+
const stats = await stat(filepath)
|
|
441
|
+
return { mtime: stats.mtime }
|
|
442
|
+
} catch (error) {
|
|
443
|
+
return null
|
|
444
|
+
}
|
|
445
|
+
},
|
|
446
|
+
},
|
|
447
|
+
projectionStore: {
|
|
448
|
+
writeFile: async (filepath: string, content: string) => {
|
|
449
|
+
filepath = join(capsuleModuleProjectionRoot, filepath)
|
|
450
|
+
await mkdir(dirname(filepath), { recursive: true })
|
|
451
|
+
await writeFile(filepath, content, 'utf-8')
|
|
452
|
+
},
|
|
453
|
+
getStats: async (filepath: string) => {
|
|
454
|
+
filepath = join(capsuleModuleProjectionRoot, filepath)
|
|
455
|
+
try {
|
|
456
|
+
const stats = await stat(filepath)
|
|
457
|
+
return { mtime: stats.mtime }
|
|
458
|
+
} catch (error) {
|
|
459
|
+
return null
|
|
460
|
+
}
|
|
461
|
+
},
|
|
462
|
+
},
|
|
463
|
+
projectionCacheStore: {
|
|
464
|
+
writeFile: async (filepath: string, content: string) => {
|
|
465
|
+
filepath = join(spineFilesystemRoot, '.~o/encapsulate.dev/projection-cache', filepath)
|
|
466
|
+
await mkdir(dirname(filepath), { recursive: true })
|
|
467
|
+
await writeFile(filepath, content, 'utf-8')
|
|
468
|
+
},
|
|
469
|
+
readFile: async (filepath: string) => {
|
|
470
|
+
filepath = join(spineFilesystemRoot, '.~o/encapsulate.dev/projection-cache', filepath)
|
|
471
|
+
return readFile(filepath, 'utf-8')
|
|
472
|
+
},
|
|
473
|
+
getStats: async (filepath: string) => {
|
|
474
|
+
filepath = join(spineFilesystemRoot, '.~o/encapsulate.dev/projection-cache', filepath)
|
|
475
|
+
try {
|
|
476
|
+
const stats = await stat(filepath)
|
|
477
|
+
return { mtime: stats.mtime }
|
|
478
|
+
} catch (error) {
|
|
479
|
+
return null
|
|
480
|
+
}
|
|
481
|
+
},
|
|
482
|
+
},
|
|
483
|
+
spineFilesystemRoot,
|
|
484
|
+
capsuleModuleProjectionPackage,
|
|
485
|
+
timing
|
|
486
|
+
}) : undefined
|
|
487
|
+
|
|
488
|
+
timing?.recordMajor('SPINE: INITIALIZATION')
|
|
489
|
+
|
|
490
|
+
let { encapsulate, freeze, capsules } = await Spine({
|
|
491
|
+
spineFilesystemRoot,
|
|
492
|
+
timing,
|
|
493
|
+
staticAnalyzer: staticAnalysisEnabled ? StaticAnalyzer({
|
|
494
|
+
timing,
|
|
495
|
+
cacheStore: {
|
|
496
|
+
writeFile: async (filepath: string, content: string) => {
|
|
497
|
+
filepath = join(spineFilesystemRoot, '.~o/encapsulate.dev/static-analysis', filepath)
|
|
498
|
+
await mkdir(dirname(filepath), { recursive: true })
|
|
499
|
+
await writeFile(filepath, content, 'utf-8')
|
|
500
|
+
},
|
|
501
|
+
readFile: async (filepath: string) => {
|
|
502
|
+
filepath = join(spineFilesystemRoot, '.~o/encapsulate.dev/static-analysis', filepath)
|
|
503
|
+
return readFile(filepath, 'utf-8')
|
|
504
|
+
},
|
|
505
|
+
getStats: async (filepath: string) => {
|
|
506
|
+
filepath = join(spineFilesystemRoot, '.~o/encapsulate.dev/static-analysis', filepath)
|
|
507
|
+
try {
|
|
508
|
+
const stats = await stat(filepath)
|
|
509
|
+
return { mtime: stats.mtime }
|
|
510
|
+
} catch (error) {
|
|
511
|
+
// File doesn't exist
|
|
512
|
+
return null
|
|
513
|
+
}
|
|
514
|
+
},
|
|
515
|
+
},
|
|
516
|
+
spineStore: {
|
|
517
|
+
getStats: async (filepath: string) => {
|
|
518
|
+
filepath = join(spineFilesystemRoot, filepath)
|
|
519
|
+
try {
|
|
520
|
+
const stats = await stat(filepath)
|
|
521
|
+
return { mtime: stats.mtime }
|
|
522
|
+
} catch (error) {
|
|
523
|
+
// File doesn't exist
|
|
524
|
+
return null
|
|
525
|
+
}
|
|
526
|
+
},
|
|
527
|
+
},
|
|
528
|
+
}) : undefined,
|
|
529
|
+
spineContracts: spineContractInstances.encapsulation
|
|
530
|
+
})
|
|
531
|
+
sourceSpine.encapsulate = encapsulate
|
|
532
|
+
|
|
533
|
+
timing?.recordMajor('SPINE RUNTIME: INITIALIZATION')
|
|
534
|
+
|
|
535
|
+
let { run } = await SpineRuntime({
|
|
536
|
+
spineFilesystemRoot,
|
|
537
|
+
spineContracts: spineContractInstances.runtime,
|
|
538
|
+
capsules
|
|
539
|
+
})
|
|
540
|
+
|
|
541
|
+
timing?.recordMajor('CAPSULE SPINE FACTORY: READY')
|
|
542
|
+
|
|
543
|
+
const loadCapsule = async ({ capsuleSnapshot }: { capsuleSourceLineRef: string, capsuleSnapshot: any }) => {
|
|
544
|
+
|
|
545
|
+
if (!capsuleModuleProjectionRoot) {
|
|
546
|
+
throw new Error('capsuleModuleProjectionRoot must be provided to enable dynamic loading of capsules')
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
let filepath = capsuleSnapshot.spineContracts?.['#@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0']?.['#@stream44.studio/encapsulate/structs/Capsule']?.projectedCapsuleFilepath
|
|
550
|
+
|
|
551
|
+
if (!filepath) throw new Error(`Cannot load capsule. No 'filepath' found at 'spineContracts["#@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0"]["#@stream44.studio/encapsulate/structs/Capsule"].projectedCapsuleFilepath'!`)
|
|
552
|
+
|
|
553
|
+
const { capsule } = await import(join(capsuleModuleProjectionRoot, filepath))
|
|
554
|
+
|
|
555
|
+
return capsule
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
return {
|
|
559
|
+
commonSpineContractOpts,
|
|
560
|
+
CapsulePropertyTypes,
|
|
561
|
+
makeImportStack,
|
|
562
|
+
encapsulate,
|
|
563
|
+
run,
|
|
564
|
+
freeze,
|
|
565
|
+
loadCapsule,
|
|
566
|
+
spineContractInstances, // Expose for testing
|
|
567
|
+
hoistSnapshot: async ({ snapshot }: { snapshot: any }) => {
|
|
568
|
+
|
|
569
|
+
timing?.recordMajor('HOIST SNAPSHOT: START')
|
|
570
|
+
|
|
571
|
+
const result = await SpineRuntime({
|
|
572
|
+
snapshot,
|
|
573
|
+
spineContracts: spineContractInstances.runtime,
|
|
574
|
+
loadCapsule
|
|
575
|
+
})
|
|
576
|
+
|
|
577
|
+
timing?.recordMajor('HOIST SNAPSHOT: COMPLETE')
|
|
578
|
+
|
|
579
|
+
return result
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
|
|
4
|
+
export function TimingObserver({ startTime }: { startTime: number }) {
|
|
5
|
+
let lastTime = startTime
|
|
6
|
+
|
|
7
|
+
return {
|
|
8
|
+
chalk,
|
|
9
|
+
record: (step: string) => {
|
|
10
|
+
const now = Date.now()
|
|
11
|
+
const diff = now - lastTime
|
|
12
|
+
lastTime = now
|
|
13
|
+
|
|
14
|
+
const line = `[+${diff}ms] ${step}`
|
|
15
|
+
console.log(diff > 10 ? chalk.red(line) : line)
|
|
16
|
+
},
|
|
17
|
+
recordMajor: (step: string) => {
|
|
18
|
+
const now = Date.now()
|
|
19
|
+
const diff = now - lastTime
|
|
20
|
+
lastTime = now
|
|
21
|
+
|
|
22
|
+
const line = `[+${diff}ms] ${step}`
|
|
23
|
+
console.log(diff > 10 ? chalk.red(line) : chalk.cyan(line))
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|