@stream44.studio/encapsulate 0.2.0-rc.1

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,290 @@
1
+ import { CapsulePropertyTypes, join } from "../../encapsulate"
2
+
3
+ export class ContractCapsuleInstanceFactory {
4
+
5
+ protected spineContractUri: string
6
+ protected capsule: any
7
+ protected self: any
8
+ protected encapsulatedApi: Record<string, any>
9
+ protected resolve?: (uri: string, parentFilepath: string) => Promise<string>
10
+ protected importCapsule?: (filepath: string) => Promise<any>
11
+ protected spineFilesystemRoot?: string
12
+ protected freezeCapsule?: (capsule: any) => Promise<any>
13
+
14
+ constructor({ spineContractUri, capsule, self, encapsulatedApi, resolve, importCapsule, spineFilesystemRoot, freezeCapsule }: { spineContractUri: string, capsule: any, self: any, encapsulatedApi: Record<string, any>, resolve?: (uri: string, parentFilepath: string) => Promise<string>, importCapsule?: (filepath: string) => Promise<any>, spineFilesystemRoot?: string, freezeCapsule?: (capsule: any) => Promise<any> }) {
15
+ this.spineContractUri = spineContractUri
16
+ this.capsule = capsule
17
+ this.self = self
18
+ this.encapsulatedApi = encapsulatedApi
19
+ this.resolve = resolve
20
+ this.importCapsule = importCapsule
21
+ this.spineFilesystemRoot = spineFilesystemRoot
22
+ this.freezeCapsule = freezeCapsule
23
+ }
24
+
25
+ async mapProperty({ overrides, options, property }: { overrides: any, options: any, property: any }) {
26
+ if (property.definition.type === CapsulePropertyTypes.Mapping) {
27
+ await this.mapMappingProperty({ overrides, options, property })
28
+ } else if (
29
+ property.definition.type === CapsulePropertyTypes.String ||
30
+ property.definition.type === CapsulePropertyTypes.Literal
31
+ ) {
32
+ this.mapLiteralProperty({ property })
33
+ } else if (property.definition.type === CapsulePropertyTypes.Function) {
34
+ this.mapFunctionProperty({ property })
35
+ } else if (property.definition.type === CapsulePropertyTypes.GetterFunction) {
36
+ this.mapGetterFunctionProperty({ property })
37
+ }
38
+ }
39
+
40
+ protected getApiTarget({ property }: { property: any }) {
41
+ // Properties under '#' go directly on the API
42
+ // Properties under '#<uri>' go under api['#<uri>']
43
+ if (!property.propertyContractUri || property.propertyContractUri === '#') {
44
+ return this.encapsulatedApi
45
+ } else {
46
+ // Namespace under the property contract key
47
+ if (!this.encapsulatedApi[property.propertyContractUri]) {
48
+ this.encapsulatedApi[property.propertyContractUri] = {}
49
+ }
50
+ return this.encapsulatedApi[property.propertyContractUri]
51
+ }
52
+ }
53
+
54
+ protected async resolveMappedCapsule({ property }: { property: any }) {
55
+ let mappedCapsule
56
+
57
+ if (typeof property.definition.value === 'string') {
58
+ if (!this.resolve) throw new Error(`'resolve' not set!`)
59
+ if (!this.spineFilesystemRoot) throw new Error(`'spineFilesystemRoot' not set!`)
60
+ if (!this.importCapsule) throw new Error(`'importCapsule' not set!`)
61
+
62
+ const parentPath = join(this.spineFilesystemRoot, this.capsule.cst.source.moduleFilepath)
63
+ const filepath = await this.resolve(property.definition.value, parentPath)
64
+ mappedCapsule = await this.importCapsule(filepath)
65
+ } else if (
66
+ typeof property.definition.value === 'object' &&
67
+ typeof property.definition.value.capsuleSourceLineRef === 'string'
68
+ ) {
69
+ mappedCapsule = property.definition.value
70
+ } else {
71
+ throw new Error(`Unknown mapping value for property '${property.name}'!`)
72
+ }
73
+
74
+ return mappedCapsule
75
+ }
76
+
77
+ protected async extractConstants({ mappedCapsule }: { mappedCapsule: any }) {
78
+ const constants: Record<string, any> = {}
79
+
80
+ const spineContractDef = mappedCapsule.definition[this.spineContractUri]
81
+
82
+ if (!spineContractDef) {
83
+ throw new Error(`Spine contract definition not found for URI: ${this.spineContractUri}. Available keys: ${Object.keys(mappedCapsule.definition).join(', ')}`)
84
+ }
85
+
86
+ // Iterate through all keys in the spine contract definition
87
+ for (const [key, value] of Object.entries(spineContractDef)) {
88
+ if (key.startsWith('#')) {
89
+ // This is a property contract - iterate through its properties
90
+ for (const [prop, propDef] of Object.entries(value as Record<string, any>)) {
91
+ const { type, value: propValue } = propDef as any
92
+
93
+ if (typeof propValue === 'undefined') continue
94
+
95
+ if (
96
+ type === CapsulePropertyTypes.String ||
97
+ type === CapsulePropertyTypes.Literal
98
+ ) {
99
+ constants[prop] = propValue
100
+ }
101
+ }
102
+ } else {
103
+ // Regular property (backwards compatibility)
104
+ const { type, value: propValue } = value as any
105
+
106
+ if (typeof propValue === 'undefined') continue
107
+
108
+ if (
109
+ type === CapsulePropertyTypes.String ||
110
+ type === CapsulePropertyTypes.Literal
111
+ ) {
112
+ constants[key] = propValue
113
+ }
114
+ }
115
+ }
116
+
117
+ return constants
118
+ }
119
+
120
+ protected async mapMappingProperty({ overrides, options, property }: { overrides: any, options: any, property: any }) {
121
+ const mappedCapsule = await this.resolveMappedCapsule({ property })
122
+ const constants = await this.extractConstants({ mappedCapsule })
123
+
124
+ const mappingOptions = await property.definition.options?.({
125
+ constants
126
+ })
127
+
128
+ // Transform overrides if this mapping has a propertyContractDelegate
129
+ let mappedOverrides = overrides
130
+ if (property.definition.propertyContractDelegate) {
131
+ // Extract overrides for the delegate property contract and map them to '#'
132
+ // Try both capsuleSourceLineRef and capsuleName
133
+ const delegateOverrides =
134
+ overrides?.[this.capsule.encapsulateOptions.capsuleSourceLineRef]?.[property.definition.propertyContractDelegate] ||
135
+ (this.capsule.encapsulateOptions.capsuleName && overrides?.[this.capsule.encapsulateOptions.capsuleName]?.[property.definition.propertyContractDelegate])
136
+
137
+ if (delegateOverrides) {
138
+ mappedOverrides = {
139
+ ...overrides,
140
+ [mappedCapsule.capsuleSourceLineRef]: {
141
+ '#': delegateOverrides
142
+ }
143
+ }
144
+ if (mappedCapsule.encapsulateOptions.capsuleName) {
145
+ mappedOverrides[mappedCapsule.encapsulateOptions.capsuleName] = {
146
+ '#': delegateOverrides
147
+ }
148
+ }
149
+ }
150
+ }
151
+
152
+ const apiTarget = this.getApiTarget({ property })
153
+ const mappedInstance = await mappedCapsule.makeInstance({
154
+ overrides: mappedOverrides,
155
+ options: mappingOptions
156
+ })
157
+
158
+ apiTarget[property.name] = mappedInstance
159
+ // Use proxy to unwrap .api for this.self so internal references work
160
+ this.self[property.name] = mappedInstance.api ? new Proxy(mappedInstance.api, {
161
+ get: (target, prop) => {
162
+ const value = target[prop]
163
+ // Recursively unwrap nested .api objects
164
+ if (value && typeof value === 'object' && value.api) {
165
+ return value.api
166
+ }
167
+ return value
168
+ }
169
+ }) : mappedInstance
170
+
171
+ // If this mapping has a propertyContractDelegate, also mount the mapped capsule's API
172
+ // to the property contract namespace for direct access
173
+ if (property.definition.propertyContractDelegate) {
174
+ // Create the property contract namespace if it doesn't exist
175
+ if (!this.encapsulatedApi[property.definition.propertyContractDelegate]) {
176
+ this.encapsulatedApi[property.definition.propertyContractDelegate] = {}
177
+ }
178
+
179
+ // Mount all properties from the mapped capsule's API to the property contract namespace
180
+ const delegateTarget = this.encapsulatedApi[property.definition.propertyContractDelegate]
181
+ for (const [key, value] of Object.entries(mappedInstance.api)) {
182
+ delegateTarget[key] = value
183
+ }
184
+ }
185
+ }
186
+
187
+ protected mapLiteralProperty({ property }: { property: any }) {
188
+ const apiTarget = this.getApiTarget({ property })
189
+ const value = typeof this.self[property.name] !== 'undefined'
190
+ ? this.self[property.name]
191
+ : property.definition.value
192
+
193
+ // Assign to both apiTarget and self so getter functions can access via this
194
+ apiTarget[property.name] = value
195
+ this.self[property.name] = value
196
+ }
197
+
198
+ protected mapFunctionProperty({ property }: { property: any }) {
199
+ const apiTarget = this.getApiTarget({ property })
200
+
201
+ // Create a proxy for this.self that intercepts property access
202
+ // Prefer this.self (which has unwrapped APIs) over encapsulatedApi
203
+ const selfProxy = new Proxy(this.self, {
204
+ get: (target: any, prop: string | symbol) => {
205
+ if (typeof prop === 'symbol') return target[prop]
206
+
207
+ // First check if the property exists in target (this.self)
208
+ if (prop in target) {
209
+ return target[prop]
210
+ }
211
+
212
+ // Fall back to encapsulatedApi
213
+ if (prop in this.encapsulatedApi) {
214
+ return this.encapsulatedApi[prop]
215
+ }
216
+
217
+ return undefined
218
+ }
219
+ })
220
+
221
+ apiTarget[property.name] = property.definition.value.bind(selfProxy)
222
+ }
223
+
224
+ protected mapGetterFunctionProperty({ property }: { property: any }) {
225
+ const apiTarget = this.getApiTarget({ property })
226
+ const getterFn = property.definition.value
227
+
228
+ // Create a proxy for this.self that intercepts property access
229
+ // Prefer this.self (which has unwrapped APIs) over encapsulatedApi
230
+ const selfProxy = new Proxy(this.self, {
231
+ get: (target: any, prop: string | symbol) => {
232
+ if (typeof prop === 'symbol') return target[prop]
233
+
234
+ // First check if the property exists in target (this.self)
235
+ if (prop in target) {
236
+ return target[prop]
237
+ }
238
+
239
+ // Fall back to encapsulatedApi
240
+ if (prop in this.encapsulatedApi) {
241
+ return this.encapsulatedApi[prop]
242
+ }
243
+
244
+ return undefined
245
+ }
246
+ })
247
+
248
+ // Define a lazy getter that calls the function only when accessed with proper this context
249
+ Object.defineProperty(apiTarget, property.name, {
250
+ get: () => {
251
+ return getterFn.call(selfProxy)
252
+ },
253
+ enumerable: true,
254
+ configurable: true
255
+ })
256
+ }
257
+
258
+ async freeze(options: any): Promise<any> {
259
+ return this.freezeCapsule?.(options) || {}
260
+ }
261
+
262
+ }
263
+
264
+
265
+
266
+
267
+ export function CapsuleSpineContract({ freezeCapsule, resolve, importCapsule, spineFilesystemRoot }: { freezeCapsule?: (capsule: any) => Promise<any>, resolve?: (uri: string, parentFilepath: string) => Promise<string>, importCapsule?: (filepath: string) => Promise<any>, spineFilesystemRoot?: string } = {}) {
268
+
269
+ return {
270
+ '#': CapsuleSpineContract['#'],
271
+ makeContractCapsuleInstance: ({ spineContractUri, capsule, self, encapsulatedApi }: { spineContractUri: string, capsule: any, self: any, encapsulatedApi: Record<string, any> }) => {
272
+ return new ContractCapsuleInstanceFactory({
273
+ spineContractUri,
274
+ capsule,
275
+ self,
276
+ encapsulatedApi,
277
+ resolve,
278
+ importCapsule,
279
+ spineFilesystemRoot,
280
+ freezeCapsule
281
+ })
282
+ },
283
+ hydrate: ({ capsuleSnapshot }: { capsuleSnapshot: any }): any => {
284
+
285
+ return capsuleSnapshot
286
+ }
287
+ }
288
+ }
289
+
290
+ CapsuleSpineContract['#'] = '@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0'
@@ -0,0 +1,299 @@
1
+ import { join, dirname } 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
+ // TODO: Make portable so we can run on other runtimes
11
+ const resolve = Bun.resolve
12
+
13
+
14
+ export async function CapsuleSpineFactory({
15
+ spineFilesystemRoot,
16
+ capsuleModuleProjectionRoot,
17
+ capsuleModuleProjectionPackage,
18
+ staticAnalysisEnabled = true,
19
+ onMembraneEvent,
20
+ enableCallerStackInference = false,
21
+ spineContracts,
22
+ timing: timingParam
23
+ }: {
24
+ spineFilesystemRoot: string,
25
+ capsuleModuleProjectionRoot?: string,
26
+ capsuleModuleProjectionPackage?: string,
27
+ staticAnalysisEnabled?: boolean,
28
+ onMembraneEvent?: (event: any) => void,
29
+ enableCallerStackInference?: boolean,
30
+ spineContracts: Record<string, any>,
31
+ timing?: { record: (step: string) => void, recordMajor: (step: string) => void, chalk?: any }
32
+ }) {
33
+
34
+ if (capsuleModuleProjectionRoot) capsuleModuleProjectionRoot = capsuleModuleProjectionRoot.replace(/^file:\/\//, '')
35
+ if (spineFilesystemRoot) spineFilesystemRoot = spineFilesystemRoot.replace(/^file:\/\//, '')
36
+
37
+ const timing = timingParam
38
+
39
+ timing?.recordMajor('CAPSULE SPINE FACTORY: INITIALIZATION')
40
+
41
+ const SingletonRegistry = () => {
42
+ const registry = new Map<string, Promise<any>>()
43
+
44
+ return {
45
+ async ensure(id: string, createHandler: () => Promise<any>) {
46
+ if (!registry.has(id)) {
47
+ registry.set(id, createHandler())
48
+ }
49
+ return registry.get(id)!
50
+ }
51
+ }
52
+ }
53
+
54
+ const registry = SingletonRegistry()
55
+
56
+ const spineContractInstances: {
57
+ encapsulation: Record<string, any>,
58
+ runtime: Record<string, any>
59
+ } = {
60
+ encapsulation: {},
61
+ runtime: {}
62
+ }
63
+
64
+ const sourceSpine: { encapsulate?: any } = {}
65
+ const commonSpineContractOpts = {
66
+ spineFilesystemRoot,
67
+ resolve: async (uri: string, parentFilepath: string) => {
68
+ // For relative paths, join with parent directory first
69
+ if (/^\.\.?\//.test(uri)) {
70
+ return await resolve(join(parentFilepath, '..', uri), spineFilesystemRoot)
71
+ }
72
+ // For absolute/package paths, use Bun.resolve directly
73
+ return await resolve(uri, parentFilepath)
74
+ },
75
+ importCapsule: async (filepath: string) => {
76
+ const shortPath = filepath.replace(/^.*\/genesis\//, '')
77
+ timing?.record(`importCapsule: Called for ${shortPath}`)
78
+ const result = await registry.ensure(filepath, async () => {
79
+ timing?.recordMajor(`importCapsule: Starting import for ${shortPath}`)
80
+ const importStart = Date.now()
81
+ const exports = await import(filepath)
82
+ const importDuration = Date.now() - importStart
83
+ timing?.recordMajor(`importCapsule: import() took ${importDuration}ms for ${shortPath}`)
84
+
85
+ if (importDuration > 10) {
86
+ if (timing) {
87
+ console.log(timing.chalk.red(`\n⚠️ WARNING: Slow module load detected!`))
88
+ console.log(timing.chalk.red(` Module: ${filepath}`))
89
+ console.log(timing.chalk.red(` Load time: ${importDuration}ms`))
90
+ console.log(timing.chalk.red(` Consider using dynamic imports to load heavy dependencies only when needed.\n`))
91
+ }
92
+ }
93
+
94
+ if (typeof exports.capsule !== 'function') throw new Error(`Module at '${filepath}' does not export 'capsule'!`)
95
+
96
+ const capsuleStart = Date.now()
97
+ const capsule = await exports.capsule({
98
+ encapsulate: sourceSpine.encapsulate,
99
+ CapsulePropertyTypes,
100
+ makeImportStack
101
+ })
102
+ const capsuleDuration = Date.now() - capsuleStart
103
+ timing?.recordMajor(`importCapsule: exports.capsule() took ${capsuleDuration}ms for ${shortPath}`)
104
+
105
+ timing?.record(`importCapsule: Returning result for ${shortPath}`)
106
+
107
+ return capsule
108
+ })
109
+ return result
110
+ },
111
+ encapsulateOpts: {
112
+ CapsulePropertyTypes
113
+ }
114
+ }
115
+
116
+ timing?.recordMajor('SPINE CONTRACTS: INITIALIZATION')
117
+
118
+ for (const spineContractUri in spineContracts) {
119
+ spineContractInstances.encapsulation[spineContractUri] = spineContracts[spineContractUri]({
120
+ ...commonSpineContractOpts,
121
+ freezeCapsule: async ({ spineContractUri, capsule }: { spineContractUri: string, capsule: any }): Promise<any> => {
122
+
123
+ if (!projector) {
124
+ throw new Error('capsuleModuleProjectionRoot must be provided to enable freezing')
125
+ }
126
+
127
+ let snapshotValues = {}
128
+
129
+ const projected = await projector.projectCapsule({
130
+ capsule,
131
+ capsules,
132
+ snapshotValues,
133
+ spineContractUri
134
+ })
135
+
136
+ return snapshotValues
137
+ }
138
+ })
139
+ spineContractInstances.runtime[spineContractUri] = spineContracts[spineContractUri]({
140
+ ...commonSpineContractOpts,
141
+ enableCallerStackInference,
142
+ onMembraneEvent,
143
+ })
144
+ }
145
+
146
+ timing?.recordMajor('CAPSULE MODULE PROJECTOR: INITIALIZATION')
147
+
148
+ const projector = capsuleModuleProjectionRoot ? CapsuleModuleProjector({
149
+ spineStore: {
150
+ writeFile: async (filepath: string, content: string) => {
151
+ filepath = join(spineFilesystemRoot, filepath)
152
+ await mkdir(dirname(filepath), { recursive: true })
153
+ await writeFile(filepath, content, 'utf-8')
154
+ },
155
+ getStats: async (filepath: string) => {
156
+ filepath = join(spineFilesystemRoot, filepath)
157
+ try {
158
+ const stats = await stat(filepath)
159
+ return { mtime: stats.mtime }
160
+ } catch (error) {
161
+ return null
162
+ }
163
+ },
164
+ },
165
+ projectionStore: {
166
+ writeFile: async (filepath: string, content: string) => {
167
+ filepath = join(capsuleModuleProjectionRoot, filepath)
168
+ await mkdir(dirname(filepath), { recursive: true })
169
+ await writeFile(filepath, content, 'utf-8')
170
+ },
171
+ getStats: async (filepath: string) => {
172
+ filepath = join(capsuleModuleProjectionRoot, filepath)
173
+ try {
174
+ const stats = await stat(filepath)
175
+ return { mtime: stats.mtime }
176
+ } catch (error) {
177
+ return null
178
+ }
179
+ },
180
+ },
181
+ projectionCacheStore: {
182
+ writeFile: async (filepath: string, content: string) => {
183
+ filepath = join(spineFilesystemRoot, '.~o/encapsulate.dev/projection-cache', filepath)
184
+ await mkdir(dirname(filepath), { recursive: true })
185
+ await writeFile(filepath, content, 'utf-8')
186
+ },
187
+ readFile: async (filepath: string) => {
188
+ filepath = join(spineFilesystemRoot, '.~o/encapsulate.dev/projection-cache', filepath)
189
+ return readFile(filepath, 'utf-8')
190
+ },
191
+ getStats: async (filepath: string) => {
192
+ filepath = join(spineFilesystemRoot, '.~o/encapsulate.dev/projection-cache', filepath)
193
+ try {
194
+ const stats = await stat(filepath)
195
+ return { mtime: stats.mtime }
196
+ } catch (error) {
197
+ return null
198
+ }
199
+ },
200
+ },
201
+ spineFilesystemRoot,
202
+ capsuleModuleProjectionPackage,
203
+ timing
204
+ }) : undefined
205
+
206
+ timing?.recordMajor('SPINE: INITIALIZATION')
207
+
208
+ let { encapsulate, freeze, capsules } = await Spine({
209
+ spineFilesystemRoot,
210
+ timing,
211
+ staticAnalyzer: staticAnalysisEnabled ? StaticAnalyzer({
212
+ timing,
213
+ cacheStore: {
214
+ writeFile: async (filepath: string, content: string) => {
215
+ filepath = join(spineFilesystemRoot, '.~o/encapsulate.dev/static-analysis', filepath)
216
+ await mkdir(dirname(filepath), { recursive: true })
217
+ await writeFile(filepath, content, 'utf-8')
218
+ },
219
+ readFile: async (filepath: string) => {
220
+ filepath = join(spineFilesystemRoot, '.~o/encapsulate.dev/static-analysis', filepath)
221
+ return readFile(filepath, 'utf-8')
222
+ },
223
+ getStats: async (filepath: string) => {
224
+ filepath = join(spineFilesystemRoot, '.~o/encapsulate.dev/static-analysis', filepath)
225
+ try {
226
+ const stats = await stat(filepath)
227
+ return { mtime: stats.mtime }
228
+ } catch (error) {
229
+ // File doesn't exist
230
+ return null
231
+ }
232
+ },
233
+ },
234
+ spineStore: {
235
+ getStats: async (filepath: string) => {
236
+ filepath = join(spineFilesystemRoot, filepath)
237
+ try {
238
+ const stats = await stat(filepath)
239
+ return { mtime: stats.mtime }
240
+ } catch (error) {
241
+ // File doesn't exist
242
+ return null
243
+ }
244
+ },
245
+ },
246
+ }) : undefined,
247
+ spineContracts: spineContractInstances.encapsulation
248
+ })
249
+ sourceSpine.encapsulate = encapsulate
250
+
251
+ timing?.recordMajor('SPINE RUNTIME: INITIALIZATION')
252
+
253
+ let { run } = await SpineRuntime({
254
+ spineFilesystemRoot,
255
+ spineContracts: spineContractInstances.runtime,
256
+ capsules
257
+ })
258
+
259
+ timing?.recordMajor('CAPSULE SPINE FACTORY: READY')
260
+
261
+ const loadCapsule = async ({ capsuleSnapshot }: { capsuleSourceLineRef: string, capsuleSnapshot: any }) => {
262
+
263
+ if (!capsuleModuleProjectionRoot) {
264
+ throw new Error('capsuleModuleProjectionRoot must be provided to enable dynamic loading of capsules')
265
+ }
266
+
267
+ let filepath = capsuleSnapshot.spineContracts?.['#@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0']?.['#@stream44.studio/encapsulate/structs/Capsule.v0']?.projectedCapsuleFilepath
268
+
269
+ 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.v0"].projectedCapsuleFilepath'!`)
270
+
271
+ const { capsule } = await import(join(capsuleModuleProjectionRoot, filepath))
272
+
273
+ return capsule
274
+ }
275
+
276
+ return {
277
+ commonSpineContractOpts,
278
+ CapsulePropertyTypes,
279
+ makeImportStack,
280
+ encapsulate,
281
+ run,
282
+ freeze,
283
+ loadCapsule,
284
+ hoistSnapshot: async ({ snapshot }: { snapshot: any }) => {
285
+
286
+ timing?.recordMajor('HOIST SNAPSHOT: START')
287
+
288
+ const result = await SpineRuntime({
289
+ snapshot,
290
+ spineContracts: spineContractInstances.runtime,
291
+ loadCapsule
292
+ })
293
+
294
+ timing?.recordMajor('HOIST SNAPSHOT: COMPLETE')
295
+
296
+ return result
297
+ }
298
+ }
299
+ }
@@ -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
+ }