@stream44.studio/encapsulate 0.4.0-rc.13 → 0.4.0-rc.15
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/package.json
CHANGED
package/src/encapsulate.ts
CHANGED
|
@@ -103,11 +103,15 @@ type TSpineContext = {
|
|
|
103
103
|
export const CapsulePropertyTypes = {
|
|
104
104
|
Function: 'Function' as const,
|
|
105
105
|
GetterFunction: 'GetterFunction' as const,
|
|
106
|
+
SetterFunction: 'SetterFunction' as const,
|
|
106
107
|
String: 'String' as const,
|
|
107
108
|
Mapping: 'Mapping' as const,
|
|
108
109
|
Literal: 'Literal' as const,
|
|
109
110
|
Constant: 'Constant' as const,
|
|
110
111
|
StructInit: 'StructInit' as const,
|
|
112
|
+
StructDispose: 'StructDispose' as const,
|
|
113
|
+
Init: 'Init' as const,
|
|
114
|
+
Dispose: 'Dispose' as const,
|
|
111
115
|
}
|
|
112
116
|
|
|
113
117
|
// ##################################################
|
|
@@ -232,26 +236,57 @@ export async function SpineRuntime(options: TSpineRuntimeOptions): Promise<TSpin
|
|
|
232
236
|
}
|
|
233
237
|
}
|
|
234
238
|
|
|
235
|
-
// Run StructInit functions for
|
|
236
|
-
//
|
|
239
|
+
// Run StructInit functions for struct capsules and Init functions for non-struct capsules
|
|
240
|
+
// StructInit: fires for struct-mapped capsules and any capsules they extend (top-down)
|
|
241
|
+
// Init: fires for non-struct capsules (those without StructInit)
|
|
237
242
|
const structInitVisited = new Set<any>()
|
|
238
|
-
|
|
243
|
+
const structInstances: any[] = [] // Track struct instances for StructDispose
|
|
244
|
+
const nonStructInstances: any[] = [] // Track non-struct instances for Dispose
|
|
245
|
+
|
|
246
|
+
async function runStructInits(instance: any, isStructContext: boolean = false) {
|
|
239
247
|
if (!instance || structInitVisited.has(instance)) return
|
|
240
248
|
structInitVisited.add(instance)
|
|
241
249
|
|
|
242
|
-
//
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
250
|
+
// Determine if this instance is a struct capsule (has StructInit functions)
|
|
251
|
+
const hasStructInit = instance.structInitFunctions?.length > 0
|
|
252
|
+
const isStruct = hasStructInit || isStructContext
|
|
253
|
+
|
|
254
|
+
if (isStruct) {
|
|
255
|
+
// This is a struct capsule - run StructInit
|
|
256
|
+
structInstances.push(instance)
|
|
257
|
+
if (instance.structInitFunctions?.length) {
|
|
258
|
+
for (const fn of instance.structInitFunctions) {
|
|
259
|
+
await fn()
|
|
260
|
+
}
|
|
261
|
+
// Sync self values back to encapsulatedApi for spine contracts that use
|
|
262
|
+
// direct assignment (e.g. Static contract) rather than getters
|
|
263
|
+
if (instance.spineContractCapsuleInstances) {
|
|
264
|
+
for (const sci of Object.values(instance.spineContractCapsuleInstances) as any[]) {
|
|
265
|
+
if (sci.self && sci.encapsulatedApi) {
|
|
266
|
+
for (const key of Object.keys(sci.encapsulatedApi)) {
|
|
267
|
+
if (key in sci.self && sci.encapsulatedApi[key] !== sci.self[key]) {
|
|
268
|
+
sci.encapsulatedApi[key] = sci.self[key]
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
246
274
|
}
|
|
247
|
-
|
|
248
|
-
//
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
275
|
+
} else {
|
|
276
|
+
// This is a non-struct capsule - run Init
|
|
277
|
+
nonStructInstances.push(instance)
|
|
278
|
+
if (instance.initFunctions?.length) {
|
|
279
|
+
for (const fn of instance.initFunctions) {
|
|
280
|
+
await fn()
|
|
281
|
+
}
|
|
282
|
+
// Sync self values back to encapsulatedApi
|
|
283
|
+
if (instance.spineContractCapsuleInstances) {
|
|
284
|
+
for (const sci of Object.values(instance.spineContractCapsuleInstances) as any[]) {
|
|
285
|
+
if (sci.self && sci.encapsulatedApi) {
|
|
286
|
+
for (const key of Object.keys(sci.encapsulatedApi)) {
|
|
287
|
+
if (key in sci.self && sci.encapsulatedApi[key] !== sci.self[key]) {
|
|
288
|
+
sci.encapsulatedApi[key] = sci.self[key]
|
|
289
|
+
}
|
|
255
290
|
}
|
|
256
291
|
}
|
|
257
292
|
}
|
|
@@ -259,15 +294,15 @@ export async function SpineRuntime(options: TSpineRuntimeOptions): Promise<TSpin
|
|
|
259
294
|
}
|
|
260
295
|
}
|
|
261
296
|
|
|
262
|
-
// Recurse into extended capsule instance
|
|
297
|
+
// Recurse into extended capsule instance (inherits struct context)
|
|
263
298
|
if (instance.extendedCapsuleInstance) {
|
|
264
|
-
await runStructInits(instance.extendedCapsuleInstance)
|
|
299
|
+
await runStructInits(instance.extendedCapsuleInstance, isStruct)
|
|
265
300
|
}
|
|
266
301
|
|
|
267
|
-
// Recurse into mapped capsule instances
|
|
302
|
+
// Recurse into mapped capsule instances (each determines its own struct status)
|
|
268
303
|
if (instance.mappedCapsuleInstances?.length) {
|
|
269
304
|
for (const mappedInstance of instance.mappedCapsuleInstances) {
|
|
270
|
-
await runStructInits(mappedInstance)
|
|
305
|
+
await runStructInits(mappedInstance, false)
|
|
271
306
|
}
|
|
272
307
|
}
|
|
273
308
|
}
|
|
@@ -278,6 +313,38 @@ export async function SpineRuntime(options: TSpineRuntimeOptions): Promise<TSpin
|
|
|
278
313
|
|
|
279
314
|
const result = await handler({ apis, capsules })
|
|
280
315
|
|
|
316
|
+
// Run StructDispose for struct capsules (reverse order - bottom-up)
|
|
317
|
+
for (let i = structInstances.length - 1; i >= 0; i--) {
|
|
318
|
+
const instance = structInstances[i]
|
|
319
|
+
if (instance.structDisposeFunctions?.length) {
|
|
320
|
+
for (const fn of instance.structDisposeFunctions) {
|
|
321
|
+
await fn()
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Run Dispose for non-struct capsules (reverse order - bottom-up)
|
|
327
|
+
for (let i = nonStructInstances.length - 1; i >= 0; i--) {
|
|
328
|
+
const instance = nonStructInstances[i]
|
|
329
|
+
if (instance.disposeFunctions?.length) {
|
|
330
|
+
for (const fn of instance.disposeFunctions) {
|
|
331
|
+
await fn()
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Clear all memoize timeouts to prevent memory leaks
|
|
337
|
+
for (const [, entry] of Object.entries(capsules)) {
|
|
338
|
+
const instance = (entry as any).instance
|
|
339
|
+
if (instance?.spineContractCapsuleInstances) {
|
|
340
|
+
for (const sci of Object.values(instance.spineContractCapsuleInstances) as any[]) {
|
|
341
|
+
if (typeof sci.clearMemoizeTimeouts === 'function') {
|
|
342
|
+
sci.clearMemoizeTimeouts()
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
281
348
|
return result
|
|
282
349
|
},
|
|
283
350
|
|
|
@@ -750,6 +817,9 @@ async function encapsulate(definition: TCapsuleDefinition, options: TCapsuleOpti
|
|
|
750
817
|
spineContractCapsuleInstances,
|
|
751
818
|
extendedCapsuleInstance,
|
|
752
819
|
structInitFunctions: [] as Array<() => any>,
|
|
820
|
+
structDisposeFunctions: [] as Array<() => any>,
|
|
821
|
+
initFunctions: [] as Array<() => any>,
|
|
822
|
+
disposeFunctions: [] as Array<() => any>,
|
|
753
823
|
mappedCapsuleInstances: [] as Array<any>,
|
|
754
824
|
rootCapsule: resolvedRootCapsule
|
|
755
825
|
}
|
|
@@ -809,12 +879,21 @@ async function encapsulate(definition: TCapsuleDefinition, options: TCapsuleOpti
|
|
|
809
879
|
}
|
|
810
880
|
ownSelf['#@stream44.studio/encapsulate/structs/Capsule'] = capsuleMetadataStruct
|
|
811
881
|
|
|
812
|
-
// Collect
|
|
882
|
+
// Collect lifecycle functions and mapped capsule instances from all spine contract capsule instances
|
|
813
883
|
for (const spineContractCapsuleInstance of Object.values(spineContractCapsuleInstances)) {
|
|
814
884
|
const sci = spineContractCapsuleInstance as any
|
|
815
885
|
if (sci.structInitFunctions?.length) {
|
|
816
886
|
capsuleInstance.structInitFunctions.push(...sci.structInitFunctions)
|
|
817
887
|
}
|
|
888
|
+
if (sci.structDisposeFunctions?.length) {
|
|
889
|
+
capsuleInstance.structDisposeFunctions.push(...sci.structDisposeFunctions)
|
|
890
|
+
}
|
|
891
|
+
if (sci.initFunctions?.length) {
|
|
892
|
+
capsuleInstance.initFunctions.push(...sci.initFunctions)
|
|
893
|
+
}
|
|
894
|
+
if (sci.disposeFunctions?.length) {
|
|
895
|
+
capsuleInstance.disposeFunctions.push(...sci.disposeFunctions)
|
|
896
|
+
}
|
|
818
897
|
if (sci.mappedCapsuleInstances?.length) {
|
|
819
898
|
capsuleInstance.mappedCapsuleInstances.push(...sci.mappedCapsuleInstances)
|
|
820
899
|
}
|
|
@@ -953,7 +1032,10 @@ function relative(from: string, to: string): string {
|
|
|
953
1032
|
}
|
|
954
1033
|
|
|
955
1034
|
function isObject(item: any): boolean {
|
|
956
|
-
|
|
1035
|
+
if (!item || typeof item !== 'object' || Array.isArray(item)) return false
|
|
1036
|
+
// Only deep-merge plain objects — preserve instances like Map, Set, Date, etc.
|
|
1037
|
+
const proto = Object.getPrototypeOf(item)
|
|
1038
|
+
return proto === Object.prototype || proto === null
|
|
957
1039
|
}
|
|
958
1040
|
|
|
959
1041
|
export function merge<T = any>(target: T, ...sources: any[]): T {
|
|
@@ -204,6 +204,23 @@ class MembraneContractCapsuleInstanceFactory extends ContractCapsuleInstanceFact
|
|
|
204
204
|
}
|
|
205
205
|
}
|
|
206
206
|
|
|
207
|
+
// Separate nested capsule-name-targeted options from own options
|
|
208
|
+
// Keys starting with '#' are own options for the mapped capsule
|
|
209
|
+
// Non-'#' keys are matched against capsule names in the mapping tree
|
|
210
|
+
let ownMappingOptions: Record<string, any> | undefined = undefined
|
|
211
|
+
let nestedCapsuleOptions: Record<string, any> | undefined = undefined
|
|
212
|
+
if (mappingOptions) {
|
|
213
|
+
for (const [key, value] of Object.entries(mappingOptions)) {
|
|
214
|
+
if (key.startsWith('#')) {
|
|
215
|
+
if (!ownMappingOptions) ownMappingOptions = {}
|
|
216
|
+
ownMappingOptions[key] = value
|
|
217
|
+
} else {
|
|
218
|
+
if (!nestedCapsuleOptions) nestedCapsuleOptions = {}
|
|
219
|
+
nestedCapsuleOptions[key] = value
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
207
224
|
// Transform overrides if this mapping has a propertyContractDelegate
|
|
208
225
|
let mappedOverrides = overrides
|
|
209
226
|
if (property.definition.propertyContractDelegate) {
|
|
@@ -229,9 +246,21 @@ class MembraneContractCapsuleInstanceFactory extends ContractCapsuleInstanceFact
|
|
|
229
246
|
}
|
|
230
247
|
}
|
|
231
248
|
|
|
249
|
+
// Merge nested capsule-name-targeted options into overrides
|
|
250
|
+
// These will be picked up when child capsules with matching names are instantiated
|
|
251
|
+
if (nestedCapsuleOptions) {
|
|
252
|
+
mappedOverrides = { ...mappedOverrides }
|
|
253
|
+
for (const [capsuleNameKey, capsuleOptions] of Object.entries(nestedCapsuleOptions)) {
|
|
254
|
+
mappedOverrides[capsuleNameKey] = {
|
|
255
|
+
...(mappedOverrides[capsuleNameKey] || {}),
|
|
256
|
+
...capsuleOptions
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
232
261
|
const mappedCapsuleInstance = await mappedCapsule.makeInstance({
|
|
233
262
|
overrides: mappedOverrides,
|
|
234
|
-
options:
|
|
263
|
+
options: ownMappingOptions,
|
|
235
264
|
runtimeSpineContracts: this.runtimeSpineContracts,
|
|
236
265
|
rootCapsule: this.capsuleInstance?.rootCapsule
|
|
237
266
|
})
|
|
@@ -429,6 +458,25 @@ class MembraneContractCapsuleInstanceFactory extends ContractCapsuleInstanceFact
|
|
|
429
458
|
protected mapFunctionProperty({ property }: { property: any }) {
|
|
430
459
|
const selfProxy = this.createSelfProxy()
|
|
431
460
|
const boundFunction = property.definition.value.bind(selfProxy)
|
|
461
|
+
const memoizeOption = property.definition.memoize
|
|
462
|
+
const shouldMemoize = memoizeOption === true || typeof memoizeOption === 'number'
|
|
463
|
+
const memoizeTtl = typeof memoizeOption === 'number' ? memoizeOption : null
|
|
464
|
+
const cacheKey = `function:${property.name}`
|
|
465
|
+
|
|
466
|
+
// Helper to set up TTL expiration
|
|
467
|
+
const setupTtlExpiration = () => {
|
|
468
|
+
if (memoizeTtl !== null) {
|
|
469
|
+
// Clear any existing timeout for this key
|
|
470
|
+
if (this.memoizeTimeouts.has(cacheKey)) {
|
|
471
|
+
clearTimeout(this.memoizeTimeouts.get(cacheKey))
|
|
472
|
+
}
|
|
473
|
+
const timeout = setTimeout(() => {
|
|
474
|
+
this.memoizeCache.delete(cacheKey)
|
|
475
|
+
this.memoizeTimeouts.delete(cacheKey)
|
|
476
|
+
}, memoizeTtl)
|
|
477
|
+
this.memoizeTimeouts.set(cacheKey, timeout)
|
|
478
|
+
}
|
|
479
|
+
}
|
|
432
480
|
|
|
433
481
|
const valueKey = `__value_${property.name}`
|
|
434
482
|
Object.defineProperty(this.encapsulatedApi, valueKey, {
|
|
@@ -441,6 +489,48 @@ class MembraneContractCapsuleInstanceFactory extends ContractCapsuleInstanceFact
|
|
|
441
489
|
Object.defineProperty(this.encapsulatedApi, property.name, {
|
|
442
490
|
get: () => {
|
|
443
491
|
return (...args: any[]) => {
|
|
492
|
+
// Check memoize cache first (only for no-arg calls or first call)
|
|
493
|
+
if (shouldMemoize && this.memoizeCache.has(cacheKey)) {
|
|
494
|
+
const cachedResult = this.memoizeCache.get(cacheKey)
|
|
495
|
+
|
|
496
|
+
const callEvent: any = {
|
|
497
|
+
event: 'call',
|
|
498
|
+
eventIndex: this.incrementEventIndex(),
|
|
499
|
+
target: {
|
|
500
|
+
capsuleSourceLineRef: this.encapsulateOptions.capsuleSourceLineRef,
|
|
501
|
+
spineContractCapsuleInstanceId: this.id,
|
|
502
|
+
prop: property.name,
|
|
503
|
+
},
|
|
504
|
+
args,
|
|
505
|
+
memoized: true
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
if (this.capsuleSourceNameRef) {
|
|
509
|
+
callEvent.target.capsuleSourceNameRef = this.capsuleSourceNameRef
|
|
510
|
+
}
|
|
511
|
+
if (this.capsuleSourceNameRefHash) {
|
|
512
|
+
callEvent.target.capsuleSourceNameRefHash = this.capsuleSourceNameRefHash
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
this.addCallerContextToEvent(callEvent)
|
|
516
|
+
this.onMembraneEvent?.(callEvent)
|
|
517
|
+
|
|
518
|
+
const resultEvent: any = {
|
|
519
|
+
event: 'call-result',
|
|
520
|
+
eventIndex: this.incrementEventIndex(),
|
|
521
|
+
callEventIndex: callEvent.eventIndex,
|
|
522
|
+
target: {
|
|
523
|
+
spineContractCapsuleInstanceId: this.id,
|
|
524
|
+
},
|
|
525
|
+
result: cachedResult,
|
|
526
|
+
memoized: true
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
this.onMembraneEvent?.(resultEvent)
|
|
530
|
+
|
|
531
|
+
return cachedResult
|
|
532
|
+
}
|
|
533
|
+
|
|
444
534
|
const callEvent: any = {
|
|
445
535
|
event: 'call',
|
|
446
536
|
eventIndex: this.incrementEventIndex(),
|
|
@@ -464,6 +554,12 @@ class MembraneContractCapsuleInstanceFactory extends ContractCapsuleInstanceFact
|
|
|
464
554
|
|
|
465
555
|
const result = boundFunction(...args)
|
|
466
556
|
|
|
557
|
+
// Store in memoize cache if memoize is enabled
|
|
558
|
+
if (shouldMemoize) {
|
|
559
|
+
this.memoizeCache.set(cacheKey, result)
|
|
560
|
+
setupTtlExpiration()
|
|
561
|
+
}
|
|
562
|
+
|
|
467
563
|
const resultEvent: any = {
|
|
468
564
|
event: 'call-result',
|
|
469
565
|
eventIndex: this.incrementEventIndex(),
|
|
@@ -487,12 +583,65 @@ class MembraneContractCapsuleInstanceFactory extends ContractCapsuleInstanceFact
|
|
|
487
583
|
protected mapGetterFunctionProperty({ property }: { property: any }) {
|
|
488
584
|
const getterFn = property.definition.value
|
|
489
585
|
const selfProxy = this.createSelfProxy()
|
|
586
|
+
const memoizeOption = property.definition.memoize
|
|
587
|
+
const shouldMemoize = memoizeOption === true || typeof memoizeOption === 'number'
|
|
588
|
+
const memoizeTtl = typeof memoizeOption === 'number' ? memoizeOption : null
|
|
589
|
+
const cacheKey = `getter:${property.name}`
|
|
590
|
+
|
|
591
|
+
// Helper to set up TTL expiration
|
|
592
|
+
const setupTtlExpiration = () => {
|
|
593
|
+
if (memoizeTtl !== null) {
|
|
594
|
+
// Clear any existing timeout for this key
|
|
595
|
+
if (this.memoizeTimeouts.has(cacheKey)) {
|
|
596
|
+
clearTimeout(this.memoizeTimeouts.get(cacheKey))
|
|
597
|
+
}
|
|
598
|
+
const timeout = setTimeout(() => {
|
|
599
|
+
this.memoizeCache.delete(cacheKey)
|
|
600
|
+
this.memoizeTimeouts.delete(cacheKey)
|
|
601
|
+
}, memoizeTtl)
|
|
602
|
+
this.memoizeTimeouts.set(cacheKey, timeout)
|
|
603
|
+
}
|
|
604
|
+
}
|
|
490
605
|
|
|
491
606
|
Object.defineProperty(this.encapsulatedApi, property.name, {
|
|
492
607
|
get: () => {
|
|
608
|
+
// Check memoize cache first
|
|
609
|
+
if (shouldMemoize && this.memoizeCache.has(cacheKey)) {
|
|
610
|
+
const cachedResult = this.memoizeCache.get(cacheKey)
|
|
611
|
+
|
|
612
|
+
const event: any = {
|
|
613
|
+
event: 'get',
|
|
614
|
+
eventIndex: this.incrementEventIndex(),
|
|
615
|
+
target: {
|
|
616
|
+
capsuleSourceLineRef: this.encapsulateOptions.capsuleSourceLineRef,
|
|
617
|
+
spineContractCapsuleInstanceId: this.id,
|
|
618
|
+
prop: property.name,
|
|
619
|
+
},
|
|
620
|
+
value: cachedResult,
|
|
621
|
+
memoized: true
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
if (this.capsuleSourceNameRef) {
|
|
625
|
+
event.target.capsuleSourceNameRef = this.capsuleSourceNameRef
|
|
626
|
+
}
|
|
627
|
+
if (this.capsuleSourceNameRefHash) {
|
|
628
|
+
event.target.capsuleSourceNameRefHash = this.capsuleSourceNameRefHash
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
this.addCallerContextToEvent(event)
|
|
632
|
+
this.onMembraneEvent?.(event)
|
|
633
|
+
return cachedResult
|
|
634
|
+
}
|
|
635
|
+
|
|
493
636
|
// Call the getter function lazily when accessed with proper this context
|
|
494
637
|
const result = getterFn.call(selfProxy)
|
|
495
638
|
|
|
639
|
+
// Store in memoize cache if memoize is enabled
|
|
640
|
+
if (shouldMemoize) {
|
|
641
|
+
this.memoizeCache.set(cacheKey, result)
|
|
642
|
+
setupTtlExpiration()
|
|
643
|
+
}
|
|
644
|
+
|
|
496
645
|
const event: any = {
|
|
497
646
|
event: 'get',
|
|
498
647
|
eventIndex: this.incrementEventIndex(),
|
|
@@ -524,7 +673,16 @@ class MembraneContractCapsuleInstanceFactory extends ContractCapsuleInstanceFact
|
|
|
524
673
|
if (this.ownSelf) {
|
|
525
674
|
Object.defineProperty(this.ownSelf, property.name, {
|
|
526
675
|
get: () => {
|
|
527
|
-
|
|
676
|
+
// For ownSelf, also respect memoization
|
|
677
|
+
if (shouldMemoize && this.memoizeCache.has(cacheKey)) {
|
|
678
|
+
return this.memoizeCache.get(cacheKey)
|
|
679
|
+
}
|
|
680
|
+
const result = getterFn.call(selfProxy)
|
|
681
|
+
if (shouldMemoize) {
|
|
682
|
+
this.memoizeCache.set(cacheKey, result)
|
|
683
|
+
setupTtlExpiration()
|
|
684
|
+
}
|
|
685
|
+
return result
|
|
528
686
|
},
|
|
529
687
|
enumerable: true,
|
|
530
688
|
configurable: true
|
|
@@ -19,7 +19,12 @@ export class ContractCapsuleInstanceFactory {
|
|
|
19
19
|
protected runtimeSpineContracts?: Record<string, any>
|
|
20
20
|
protected capsuleInstance?: any
|
|
21
21
|
public structInitFunctions: Array<() => any> = []
|
|
22
|
+
public structDisposeFunctions: Array<() => any> = []
|
|
23
|
+
public initFunctions: Array<() => any> = []
|
|
24
|
+
public disposeFunctions: Array<() => any> = []
|
|
22
25
|
public mappedCapsuleInstances: Array<any> = []
|
|
26
|
+
protected memoizeCache: Map<string, any> = new Map()
|
|
27
|
+
protected memoizeTimeouts: Map<string, ReturnType<typeof setTimeout>> = new Map()
|
|
23
28
|
|
|
24
29
|
constructor({ spineContractUri, capsule, self, ownSelf, encapsulatedApi, resolve, importCapsule, spineFilesystemRoot, freezeCapsule, instanceRegistry, extendedCapsuleInstance, runtimeSpineContracts, capsuleInstance }: { spineContractUri: string, capsule: any, self: any, ownSelf?: any, encapsulatedApi: Record<string, any>, resolve?: (uri: string, parentFilepath: string) => Promise<string>, importCapsule?: (filepath: string) => Promise<any>, spineFilesystemRoot?: string, freezeCapsule?: (capsule: any) => Promise<any>, instanceRegistry?: CapsuleInstanceRegistry, extendedCapsuleInstance?: any, runtimeSpineContracts?: Record<string, any>, capsuleInstance?: any }) {
|
|
25
30
|
this.spineContractUri = spineContractUri
|
|
@@ -50,8 +55,16 @@ export class ContractCapsuleInstanceFactory {
|
|
|
50
55
|
this.mapFunctionProperty({ property })
|
|
51
56
|
} else if (property.definition.type === CapsulePropertyTypes.GetterFunction) {
|
|
52
57
|
this.mapGetterFunctionProperty({ property })
|
|
58
|
+
} else if (property.definition.type === CapsulePropertyTypes.SetterFunction) {
|
|
59
|
+
this.mapSetterFunctionProperty({ property })
|
|
53
60
|
} else if (property.definition.type === CapsulePropertyTypes.StructInit) {
|
|
54
61
|
this.mapStructInitProperty({ property })
|
|
62
|
+
} else if (property.definition.type === CapsulePropertyTypes.StructDispose) {
|
|
63
|
+
this.mapStructDisposeProperty({ property })
|
|
64
|
+
} else if (property.definition.type === CapsulePropertyTypes.Init) {
|
|
65
|
+
this.mapInitProperty({ property })
|
|
66
|
+
} else if (property.definition.type === CapsulePropertyTypes.Dispose) {
|
|
67
|
+
this.mapDisposeProperty({ property })
|
|
55
68
|
}
|
|
56
69
|
}
|
|
57
70
|
|
|
@@ -201,6 +214,23 @@ export class ContractCapsuleInstanceFactory {
|
|
|
201
214
|
}
|
|
202
215
|
}
|
|
203
216
|
|
|
217
|
+
// Separate nested capsule-name-targeted options from own options
|
|
218
|
+
// Keys starting with '#' are own options for the mapped capsule
|
|
219
|
+
// Non-'#' keys are matched against capsule names in the mapping tree
|
|
220
|
+
let ownMappingOptions: Record<string, any> | undefined = undefined
|
|
221
|
+
let nestedCapsuleOptions: Record<string, any> | undefined = undefined
|
|
222
|
+
if (mappingOptions) {
|
|
223
|
+
for (const [key, value] of Object.entries(mappingOptions)) {
|
|
224
|
+
if (key.startsWith('#')) {
|
|
225
|
+
if (!ownMappingOptions) ownMappingOptions = {}
|
|
226
|
+
ownMappingOptions[key] = value
|
|
227
|
+
} else {
|
|
228
|
+
if (!nestedCapsuleOptions) nestedCapsuleOptions = {}
|
|
229
|
+
nestedCapsuleOptions[key] = value
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
204
234
|
// Transform overrides if this mapping has a propertyContractDelegate
|
|
205
235
|
let mappedOverrides = overrides
|
|
206
236
|
if (property.definition.propertyContractDelegate) {
|
|
@@ -225,10 +255,22 @@ export class ContractCapsuleInstanceFactory {
|
|
|
225
255
|
}
|
|
226
256
|
}
|
|
227
257
|
|
|
258
|
+
// Merge nested capsule-name-targeted options into overrides
|
|
259
|
+
// These will be picked up when child capsules with matching names are instantiated
|
|
260
|
+
if (nestedCapsuleOptions) {
|
|
261
|
+
mappedOverrides = { ...mappedOverrides }
|
|
262
|
+
for (const [capsuleNameKey, capsuleOptions] of Object.entries(nestedCapsuleOptions)) {
|
|
263
|
+
mappedOverrides[capsuleNameKey] = {
|
|
264
|
+
...(mappedOverrides[capsuleNameKey] || {}),
|
|
265
|
+
...capsuleOptions
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
228
270
|
const apiTarget = this.getApiTarget({ property })
|
|
229
271
|
const mappedInstance = await mappedCapsule.makeInstance({
|
|
230
272
|
overrides: mappedOverrides,
|
|
231
|
-
options:
|
|
273
|
+
options: ownMappingOptions,
|
|
232
274
|
runtimeSpineContracts: this.runtimeSpineContracts,
|
|
233
275
|
rootCapsule: this.capsuleInstance?.rootCapsule
|
|
234
276
|
})
|
|
@@ -321,18 +363,82 @@ export class ContractCapsuleInstanceFactory {
|
|
|
321
363
|
protected mapFunctionProperty({ property }: { property: any }) {
|
|
322
364
|
const apiTarget = this.getApiTarget({ property })
|
|
323
365
|
const selfProxy = this.createSelfProxy()
|
|
324
|
-
|
|
366
|
+
const boundFunction = property.definition.value.bind(selfProxy)
|
|
367
|
+
const memoizeOption = property.definition.memoize
|
|
368
|
+
const shouldMemoize = memoizeOption === true || typeof memoizeOption === 'number'
|
|
369
|
+
const memoizeTtl = typeof memoizeOption === 'number' ? memoizeOption : null
|
|
370
|
+
const cacheKey = `function:${property.name}`
|
|
371
|
+
|
|
372
|
+
if (shouldMemoize) {
|
|
373
|
+
// Wrap the function to support memoization
|
|
374
|
+
apiTarget[property.name] = (...args: any[]) => {
|
|
375
|
+
if (this.memoizeCache.has(cacheKey)) {
|
|
376
|
+
return this.memoizeCache.get(cacheKey)
|
|
377
|
+
}
|
|
378
|
+
const result = boundFunction(...args)
|
|
379
|
+
this.memoizeCache.set(cacheKey, result)
|
|
380
|
+
|
|
381
|
+
// Set up TTL expiration if specified
|
|
382
|
+
if (memoizeTtl !== null) {
|
|
383
|
+
// Clear any existing timeout for this key
|
|
384
|
+
if (this.memoizeTimeouts.has(cacheKey)) {
|
|
385
|
+
clearTimeout(this.memoizeTimeouts.get(cacheKey))
|
|
386
|
+
}
|
|
387
|
+
const timeout = setTimeout(() => {
|
|
388
|
+
this.memoizeCache.delete(cacheKey)
|
|
389
|
+
this.memoizeTimeouts.delete(cacheKey)
|
|
390
|
+
}, memoizeTtl)
|
|
391
|
+
this.memoizeTimeouts.set(cacheKey, timeout)
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
return result
|
|
395
|
+
}
|
|
396
|
+
} else {
|
|
397
|
+
apiTarget[property.name] = boundFunction
|
|
398
|
+
}
|
|
325
399
|
}
|
|
326
400
|
|
|
327
401
|
protected mapGetterFunctionProperty({ property }: { property: any }) {
|
|
328
402
|
const apiTarget = this.getApiTarget({ property })
|
|
329
403
|
const getterFn = property.definition.value
|
|
330
404
|
const selfProxy = this.createSelfProxy()
|
|
405
|
+
const memoizeOption = property.definition.memoize
|
|
406
|
+
const shouldMemoize = memoizeOption === true || typeof memoizeOption === 'number'
|
|
407
|
+
const memoizeTtl = typeof memoizeOption === 'number' ? memoizeOption : null
|
|
408
|
+
const cacheKey = `getter:${property.name}`
|
|
409
|
+
|
|
410
|
+
// Helper to set up TTL expiration
|
|
411
|
+
const setupTtlExpiration = () => {
|
|
412
|
+
if (memoizeTtl !== null) {
|
|
413
|
+
// Clear any existing timeout for this key
|
|
414
|
+
if (this.memoizeTimeouts.has(cacheKey)) {
|
|
415
|
+
clearTimeout(this.memoizeTimeouts.get(cacheKey))
|
|
416
|
+
}
|
|
417
|
+
const timeout = setTimeout(() => {
|
|
418
|
+
this.memoizeCache.delete(cacheKey)
|
|
419
|
+
this.memoizeTimeouts.delete(cacheKey)
|
|
420
|
+
}, memoizeTtl)
|
|
421
|
+
this.memoizeTimeouts.set(cacheKey, timeout)
|
|
422
|
+
}
|
|
423
|
+
}
|
|
331
424
|
|
|
332
425
|
// Define a lazy getter that calls the function only when accessed with proper this context
|
|
333
426
|
Object.defineProperty(apiTarget, property.name, {
|
|
334
427
|
get: () => {
|
|
335
|
-
|
|
428
|
+
// Check memoize cache first
|
|
429
|
+
if (shouldMemoize && this.memoizeCache.has(cacheKey)) {
|
|
430
|
+
return this.memoizeCache.get(cacheKey)
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const result = getterFn.call(selfProxy)
|
|
434
|
+
|
|
435
|
+
// Store in memoize cache if memoize is enabled
|
|
436
|
+
if (shouldMemoize) {
|
|
437
|
+
this.memoizeCache.set(cacheKey, result)
|
|
438
|
+
setupTtlExpiration()
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
return result
|
|
336
442
|
},
|
|
337
443
|
enumerable: true,
|
|
338
444
|
configurable: true
|
|
@@ -343,7 +449,42 @@ export class ContractCapsuleInstanceFactory {
|
|
|
343
449
|
if (this.ownSelf) {
|
|
344
450
|
Object.defineProperty(this.ownSelf, property.name, {
|
|
345
451
|
get: () => {
|
|
346
|
-
|
|
452
|
+
// For ownSelf, also respect memoization
|
|
453
|
+
if (shouldMemoize && this.memoizeCache.has(cacheKey)) {
|
|
454
|
+
return this.memoizeCache.get(cacheKey)
|
|
455
|
+
}
|
|
456
|
+
const result = getterFn.call(selfProxy)
|
|
457
|
+
if (shouldMemoize) {
|
|
458
|
+
this.memoizeCache.set(cacheKey, result)
|
|
459
|
+
setupTtlExpiration()
|
|
460
|
+
}
|
|
461
|
+
return result
|
|
462
|
+
},
|
|
463
|
+
enumerable: true,
|
|
464
|
+
configurable: true
|
|
465
|
+
})
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
protected mapSetterFunctionProperty({ property }: { property: any }) {
|
|
470
|
+
const apiTarget = this.getApiTarget({ property })
|
|
471
|
+
const setterFn = property.definition.value
|
|
472
|
+
const selfProxy = this.createSelfProxy()
|
|
473
|
+
|
|
474
|
+
// Define a setter that calls the function when the property is assigned
|
|
475
|
+
Object.defineProperty(apiTarget, property.name, {
|
|
476
|
+
set: (value: any) => {
|
|
477
|
+
setterFn.call(selfProxy, value)
|
|
478
|
+
},
|
|
479
|
+
enumerable: true,
|
|
480
|
+
configurable: true
|
|
481
|
+
})
|
|
482
|
+
|
|
483
|
+
// Also define the setter on ownSelf so this.self.propertyName = value works
|
|
484
|
+
if (this.ownSelf) {
|
|
485
|
+
Object.defineProperty(this.ownSelf, property.name, {
|
|
486
|
+
set: (value: any) => {
|
|
487
|
+
setterFn.call(selfProxy, value)
|
|
347
488
|
},
|
|
348
489
|
enumerable: true,
|
|
349
490
|
configurable: true
|
|
@@ -357,10 +498,35 @@ export class ContractCapsuleInstanceFactory {
|
|
|
357
498
|
this.structInitFunctions.push(boundFunction)
|
|
358
499
|
}
|
|
359
500
|
|
|
501
|
+
protected mapStructDisposeProperty({ property }: { property: any }) {
|
|
502
|
+
const selfProxy = this.createSelfProxy()
|
|
503
|
+
const boundFunction = property.definition.value.bind(selfProxy)
|
|
504
|
+
this.structDisposeFunctions.push(boundFunction)
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
protected mapInitProperty({ property }: { property: any }) {
|
|
508
|
+
const selfProxy = this.createSelfProxy()
|
|
509
|
+
const boundFunction = property.definition.value.bind(selfProxy)
|
|
510
|
+
this.initFunctions.push(boundFunction)
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
protected mapDisposeProperty({ property }: { property: any }) {
|
|
514
|
+
const selfProxy = this.createSelfProxy()
|
|
515
|
+
const boundFunction = property.definition.value.bind(selfProxy)
|
|
516
|
+
this.disposeFunctions.push(boundFunction)
|
|
517
|
+
}
|
|
518
|
+
|
|
360
519
|
async freeze(options: any): Promise<any> {
|
|
361
520
|
return this.freezeCapsule?.(options) || {}
|
|
362
521
|
}
|
|
363
522
|
|
|
523
|
+
public clearMemoizeTimeouts() {
|
|
524
|
+
for (const timeout of this.memoizeTimeouts.values()) {
|
|
525
|
+
clearTimeout(timeout)
|
|
526
|
+
}
|
|
527
|
+
this.memoizeTimeouts.clear()
|
|
528
|
+
}
|
|
529
|
+
|
|
364
530
|
}
|
|
365
531
|
|
|
366
532
|
|