@stream44.studio/encapsulate 0.4.0-rc.13 → 0.4.0-rc.14
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 {
|
|
@@ -429,6 +429,25 @@ class MembraneContractCapsuleInstanceFactory extends ContractCapsuleInstanceFact
|
|
|
429
429
|
protected mapFunctionProperty({ property }: { property: any }) {
|
|
430
430
|
const selfProxy = this.createSelfProxy()
|
|
431
431
|
const boundFunction = property.definition.value.bind(selfProxy)
|
|
432
|
+
const memoizeOption = property.definition.memoize
|
|
433
|
+
const shouldMemoize = memoizeOption === true || typeof memoizeOption === 'number'
|
|
434
|
+
const memoizeTtl = typeof memoizeOption === 'number' ? memoizeOption : null
|
|
435
|
+
const cacheKey = `function:${property.name}`
|
|
436
|
+
|
|
437
|
+
// Helper to set up TTL expiration
|
|
438
|
+
const setupTtlExpiration = () => {
|
|
439
|
+
if (memoizeTtl !== null) {
|
|
440
|
+
// Clear any existing timeout for this key
|
|
441
|
+
if (this.memoizeTimeouts.has(cacheKey)) {
|
|
442
|
+
clearTimeout(this.memoizeTimeouts.get(cacheKey))
|
|
443
|
+
}
|
|
444
|
+
const timeout = setTimeout(() => {
|
|
445
|
+
this.memoizeCache.delete(cacheKey)
|
|
446
|
+
this.memoizeTimeouts.delete(cacheKey)
|
|
447
|
+
}, memoizeTtl)
|
|
448
|
+
this.memoizeTimeouts.set(cacheKey, timeout)
|
|
449
|
+
}
|
|
450
|
+
}
|
|
432
451
|
|
|
433
452
|
const valueKey = `__value_${property.name}`
|
|
434
453
|
Object.defineProperty(this.encapsulatedApi, valueKey, {
|
|
@@ -441,6 +460,48 @@ class MembraneContractCapsuleInstanceFactory extends ContractCapsuleInstanceFact
|
|
|
441
460
|
Object.defineProperty(this.encapsulatedApi, property.name, {
|
|
442
461
|
get: () => {
|
|
443
462
|
return (...args: any[]) => {
|
|
463
|
+
// Check memoize cache first (only for no-arg calls or first call)
|
|
464
|
+
if (shouldMemoize && this.memoizeCache.has(cacheKey)) {
|
|
465
|
+
const cachedResult = this.memoizeCache.get(cacheKey)
|
|
466
|
+
|
|
467
|
+
const callEvent: any = {
|
|
468
|
+
event: 'call',
|
|
469
|
+
eventIndex: this.incrementEventIndex(),
|
|
470
|
+
target: {
|
|
471
|
+
capsuleSourceLineRef: this.encapsulateOptions.capsuleSourceLineRef,
|
|
472
|
+
spineContractCapsuleInstanceId: this.id,
|
|
473
|
+
prop: property.name,
|
|
474
|
+
},
|
|
475
|
+
args,
|
|
476
|
+
memoized: true
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
if (this.capsuleSourceNameRef) {
|
|
480
|
+
callEvent.target.capsuleSourceNameRef = this.capsuleSourceNameRef
|
|
481
|
+
}
|
|
482
|
+
if (this.capsuleSourceNameRefHash) {
|
|
483
|
+
callEvent.target.capsuleSourceNameRefHash = this.capsuleSourceNameRefHash
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
this.addCallerContextToEvent(callEvent)
|
|
487
|
+
this.onMembraneEvent?.(callEvent)
|
|
488
|
+
|
|
489
|
+
const resultEvent: any = {
|
|
490
|
+
event: 'call-result',
|
|
491
|
+
eventIndex: this.incrementEventIndex(),
|
|
492
|
+
callEventIndex: callEvent.eventIndex,
|
|
493
|
+
target: {
|
|
494
|
+
spineContractCapsuleInstanceId: this.id,
|
|
495
|
+
},
|
|
496
|
+
result: cachedResult,
|
|
497
|
+
memoized: true
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
this.onMembraneEvent?.(resultEvent)
|
|
501
|
+
|
|
502
|
+
return cachedResult
|
|
503
|
+
}
|
|
504
|
+
|
|
444
505
|
const callEvent: any = {
|
|
445
506
|
event: 'call',
|
|
446
507
|
eventIndex: this.incrementEventIndex(),
|
|
@@ -464,6 +525,12 @@ class MembraneContractCapsuleInstanceFactory extends ContractCapsuleInstanceFact
|
|
|
464
525
|
|
|
465
526
|
const result = boundFunction(...args)
|
|
466
527
|
|
|
528
|
+
// Store in memoize cache if memoize is enabled
|
|
529
|
+
if (shouldMemoize) {
|
|
530
|
+
this.memoizeCache.set(cacheKey, result)
|
|
531
|
+
setupTtlExpiration()
|
|
532
|
+
}
|
|
533
|
+
|
|
467
534
|
const resultEvent: any = {
|
|
468
535
|
event: 'call-result',
|
|
469
536
|
eventIndex: this.incrementEventIndex(),
|
|
@@ -487,12 +554,65 @@ class MembraneContractCapsuleInstanceFactory extends ContractCapsuleInstanceFact
|
|
|
487
554
|
protected mapGetterFunctionProperty({ property }: { property: any }) {
|
|
488
555
|
const getterFn = property.definition.value
|
|
489
556
|
const selfProxy = this.createSelfProxy()
|
|
557
|
+
const memoizeOption = property.definition.memoize
|
|
558
|
+
const shouldMemoize = memoizeOption === true || typeof memoizeOption === 'number'
|
|
559
|
+
const memoizeTtl = typeof memoizeOption === 'number' ? memoizeOption : null
|
|
560
|
+
const cacheKey = `getter:${property.name}`
|
|
561
|
+
|
|
562
|
+
// Helper to set up TTL expiration
|
|
563
|
+
const setupTtlExpiration = () => {
|
|
564
|
+
if (memoizeTtl !== null) {
|
|
565
|
+
// Clear any existing timeout for this key
|
|
566
|
+
if (this.memoizeTimeouts.has(cacheKey)) {
|
|
567
|
+
clearTimeout(this.memoizeTimeouts.get(cacheKey))
|
|
568
|
+
}
|
|
569
|
+
const timeout = setTimeout(() => {
|
|
570
|
+
this.memoizeCache.delete(cacheKey)
|
|
571
|
+
this.memoizeTimeouts.delete(cacheKey)
|
|
572
|
+
}, memoizeTtl)
|
|
573
|
+
this.memoizeTimeouts.set(cacheKey, timeout)
|
|
574
|
+
}
|
|
575
|
+
}
|
|
490
576
|
|
|
491
577
|
Object.defineProperty(this.encapsulatedApi, property.name, {
|
|
492
578
|
get: () => {
|
|
579
|
+
// Check memoize cache first
|
|
580
|
+
if (shouldMemoize && this.memoizeCache.has(cacheKey)) {
|
|
581
|
+
const cachedResult = this.memoizeCache.get(cacheKey)
|
|
582
|
+
|
|
583
|
+
const event: any = {
|
|
584
|
+
event: 'get',
|
|
585
|
+
eventIndex: this.incrementEventIndex(),
|
|
586
|
+
target: {
|
|
587
|
+
capsuleSourceLineRef: this.encapsulateOptions.capsuleSourceLineRef,
|
|
588
|
+
spineContractCapsuleInstanceId: this.id,
|
|
589
|
+
prop: property.name,
|
|
590
|
+
},
|
|
591
|
+
value: cachedResult,
|
|
592
|
+
memoized: true
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
if (this.capsuleSourceNameRef) {
|
|
596
|
+
event.target.capsuleSourceNameRef = this.capsuleSourceNameRef
|
|
597
|
+
}
|
|
598
|
+
if (this.capsuleSourceNameRefHash) {
|
|
599
|
+
event.target.capsuleSourceNameRefHash = this.capsuleSourceNameRefHash
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
this.addCallerContextToEvent(event)
|
|
603
|
+
this.onMembraneEvent?.(event)
|
|
604
|
+
return cachedResult
|
|
605
|
+
}
|
|
606
|
+
|
|
493
607
|
// Call the getter function lazily when accessed with proper this context
|
|
494
608
|
const result = getterFn.call(selfProxy)
|
|
495
609
|
|
|
610
|
+
// Store in memoize cache if memoize is enabled
|
|
611
|
+
if (shouldMemoize) {
|
|
612
|
+
this.memoizeCache.set(cacheKey, result)
|
|
613
|
+
setupTtlExpiration()
|
|
614
|
+
}
|
|
615
|
+
|
|
496
616
|
const event: any = {
|
|
497
617
|
event: 'get',
|
|
498
618
|
eventIndex: this.incrementEventIndex(),
|
|
@@ -524,7 +644,16 @@ class MembraneContractCapsuleInstanceFactory extends ContractCapsuleInstanceFact
|
|
|
524
644
|
if (this.ownSelf) {
|
|
525
645
|
Object.defineProperty(this.ownSelf, property.name, {
|
|
526
646
|
get: () => {
|
|
527
|
-
|
|
647
|
+
// For ownSelf, also respect memoization
|
|
648
|
+
if (shouldMemoize && this.memoizeCache.has(cacheKey)) {
|
|
649
|
+
return this.memoizeCache.get(cacheKey)
|
|
650
|
+
}
|
|
651
|
+
const result = getterFn.call(selfProxy)
|
|
652
|
+
if (shouldMemoize) {
|
|
653
|
+
this.memoizeCache.set(cacheKey, result)
|
|
654
|
+
setupTtlExpiration()
|
|
655
|
+
}
|
|
656
|
+
return result
|
|
528
657
|
},
|
|
529
658
|
enumerable: true,
|
|
530
659
|
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
|
|
|
@@ -321,18 +334,82 @@ export class ContractCapsuleInstanceFactory {
|
|
|
321
334
|
protected mapFunctionProperty({ property }: { property: any }) {
|
|
322
335
|
const apiTarget = this.getApiTarget({ property })
|
|
323
336
|
const selfProxy = this.createSelfProxy()
|
|
324
|
-
|
|
337
|
+
const boundFunction = property.definition.value.bind(selfProxy)
|
|
338
|
+
const memoizeOption = property.definition.memoize
|
|
339
|
+
const shouldMemoize = memoizeOption === true || typeof memoizeOption === 'number'
|
|
340
|
+
const memoizeTtl = typeof memoizeOption === 'number' ? memoizeOption : null
|
|
341
|
+
const cacheKey = `function:${property.name}`
|
|
342
|
+
|
|
343
|
+
if (shouldMemoize) {
|
|
344
|
+
// Wrap the function to support memoization
|
|
345
|
+
apiTarget[property.name] = (...args: any[]) => {
|
|
346
|
+
if (this.memoizeCache.has(cacheKey)) {
|
|
347
|
+
return this.memoizeCache.get(cacheKey)
|
|
348
|
+
}
|
|
349
|
+
const result = boundFunction(...args)
|
|
350
|
+
this.memoizeCache.set(cacheKey, result)
|
|
351
|
+
|
|
352
|
+
// Set up TTL expiration if specified
|
|
353
|
+
if (memoizeTtl !== null) {
|
|
354
|
+
// Clear any existing timeout for this key
|
|
355
|
+
if (this.memoizeTimeouts.has(cacheKey)) {
|
|
356
|
+
clearTimeout(this.memoizeTimeouts.get(cacheKey))
|
|
357
|
+
}
|
|
358
|
+
const timeout = setTimeout(() => {
|
|
359
|
+
this.memoizeCache.delete(cacheKey)
|
|
360
|
+
this.memoizeTimeouts.delete(cacheKey)
|
|
361
|
+
}, memoizeTtl)
|
|
362
|
+
this.memoizeTimeouts.set(cacheKey, timeout)
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return result
|
|
366
|
+
}
|
|
367
|
+
} else {
|
|
368
|
+
apiTarget[property.name] = boundFunction
|
|
369
|
+
}
|
|
325
370
|
}
|
|
326
371
|
|
|
327
372
|
protected mapGetterFunctionProperty({ property }: { property: any }) {
|
|
328
373
|
const apiTarget = this.getApiTarget({ property })
|
|
329
374
|
const getterFn = property.definition.value
|
|
330
375
|
const selfProxy = this.createSelfProxy()
|
|
376
|
+
const memoizeOption = property.definition.memoize
|
|
377
|
+
const shouldMemoize = memoizeOption === true || typeof memoizeOption === 'number'
|
|
378
|
+
const memoizeTtl = typeof memoizeOption === 'number' ? memoizeOption : null
|
|
379
|
+
const cacheKey = `getter:${property.name}`
|
|
380
|
+
|
|
381
|
+
// Helper to set up TTL expiration
|
|
382
|
+
const setupTtlExpiration = () => {
|
|
383
|
+
if (memoizeTtl !== null) {
|
|
384
|
+
// Clear any existing timeout for this key
|
|
385
|
+
if (this.memoizeTimeouts.has(cacheKey)) {
|
|
386
|
+
clearTimeout(this.memoizeTimeouts.get(cacheKey))
|
|
387
|
+
}
|
|
388
|
+
const timeout = setTimeout(() => {
|
|
389
|
+
this.memoizeCache.delete(cacheKey)
|
|
390
|
+
this.memoizeTimeouts.delete(cacheKey)
|
|
391
|
+
}, memoizeTtl)
|
|
392
|
+
this.memoizeTimeouts.set(cacheKey, timeout)
|
|
393
|
+
}
|
|
394
|
+
}
|
|
331
395
|
|
|
332
396
|
// Define a lazy getter that calls the function only when accessed with proper this context
|
|
333
397
|
Object.defineProperty(apiTarget, property.name, {
|
|
334
398
|
get: () => {
|
|
335
|
-
|
|
399
|
+
// Check memoize cache first
|
|
400
|
+
if (shouldMemoize && this.memoizeCache.has(cacheKey)) {
|
|
401
|
+
return this.memoizeCache.get(cacheKey)
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const result = getterFn.call(selfProxy)
|
|
405
|
+
|
|
406
|
+
// Store in memoize cache if memoize is enabled
|
|
407
|
+
if (shouldMemoize) {
|
|
408
|
+
this.memoizeCache.set(cacheKey, result)
|
|
409
|
+
setupTtlExpiration()
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
return result
|
|
336
413
|
},
|
|
337
414
|
enumerable: true,
|
|
338
415
|
configurable: true
|
|
@@ -343,7 +420,42 @@ export class ContractCapsuleInstanceFactory {
|
|
|
343
420
|
if (this.ownSelf) {
|
|
344
421
|
Object.defineProperty(this.ownSelf, property.name, {
|
|
345
422
|
get: () => {
|
|
346
|
-
|
|
423
|
+
// For ownSelf, also respect memoization
|
|
424
|
+
if (shouldMemoize && this.memoizeCache.has(cacheKey)) {
|
|
425
|
+
return this.memoizeCache.get(cacheKey)
|
|
426
|
+
}
|
|
427
|
+
const result = getterFn.call(selfProxy)
|
|
428
|
+
if (shouldMemoize) {
|
|
429
|
+
this.memoizeCache.set(cacheKey, result)
|
|
430
|
+
setupTtlExpiration()
|
|
431
|
+
}
|
|
432
|
+
return result
|
|
433
|
+
},
|
|
434
|
+
enumerable: true,
|
|
435
|
+
configurable: true
|
|
436
|
+
})
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
protected mapSetterFunctionProperty({ property }: { property: any }) {
|
|
441
|
+
const apiTarget = this.getApiTarget({ property })
|
|
442
|
+
const setterFn = property.definition.value
|
|
443
|
+
const selfProxy = this.createSelfProxy()
|
|
444
|
+
|
|
445
|
+
// Define a setter that calls the function when the property is assigned
|
|
446
|
+
Object.defineProperty(apiTarget, property.name, {
|
|
447
|
+
set: (value: any) => {
|
|
448
|
+
setterFn.call(selfProxy, value)
|
|
449
|
+
},
|
|
450
|
+
enumerable: true,
|
|
451
|
+
configurable: true
|
|
452
|
+
})
|
|
453
|
+
|
|
454
|
+
// Also define the setter on ownSelf so this.self.propertyName = value works
|
|
455
|
+
if (this.ownSelf) {
|
|
456
|
+
Object.defineProperty(this.ownSelf, property.name, {
|
|
457
|
+
set: (value: any) => {
|
|
458
|
+
setterFn.call(selfProxy, value)
|
|
347
459
|
},
|
|
348
460
|
enumerable: true,
|
|
349
461
|
configurable: true
|
|
@@ -357,10 +469,35 @@ export class ContractCapsuleInstanceFactory {
|
|
|
357
469
|
this.structInitFunctions.push(boundFunction)
|
|
358
470
|
}
|
|
359
471
|
|
|
472
|
+
protected mapStructDisposeProperty({ property }: { property: any }) {
|
|
473
|
+
const selfProxy = this.createSelfProxy()
|
|
474
|
+
const boundFunction = property.definition.value.bind(selfProxy)
|
|
475
|
+
this.structDisposeFunctions.push(boundFunction)
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
protected mapInitProperty({ property }: { property: any }) {
|
|
479
|
+
const selfProxy = this.createSelfProxy()
|
|
480
|
+
const boundFunction = property.definition.value.bind(selfProxy)
|
|
481
|
+
this.initFunctions.push(boundFunction)
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
protected mapDisposeProperty({ property }: { property: any }) {
|
|
485
|
+
const selfProxy = this.createSelfProxy()
|
|
486
|
+
const boundFunction = property.definition.value.bind(selfProxy)
|
|
487
|
+
this.disposeFunctions.push(boundFunction)
|
|
488
|
+
}
|
|
489
|
+
|
|
360
490
|
async freeze(options: any): Promise<any> {
|
|
361
491
|
return this.freezeCapsule?.(options) || {}
|
|
362
492
|
}
|
|
363
493
|
|
|
494
|
+
public clearMemoizeTimeouts() {
|
|
495
|
+
for (const timeout of this.memoizeTimeouts.values()) {
|
|
496
|
+
clearTimeout(timeout)
|
|
497
|
+
}
|
|
498
|
+
this.memoizeTimeouts.clear()
|
|
499
|
+
}
|
|
500
|
+
|
|
364
501
|
}
|
|
365
502
|
|
|
366
503
|
|