@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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stream44.studio/encapsulate",
3
- "version": "0.4.0-rc.13",
3
+ "version": "0.4.0-rc.14",
4
4
  "license": "BSD-2-Clause-Patent",
5
5
  "repository": {
6
6
  "type": "git",
@@ -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 the entire capsule tree (top-down)
236
- // Top-down means: child capsule's StructInit runs before extended parent's StructInit
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
- async function runStructInits(instance: any) {
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
- // Run this capsule's own StructInit functions first (top-down)
243
- if (instance.structInitFunctions?.length) {
244
- for (const fn of instance.structInitFunctions) {
245
- await fn()
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
- // Sync self values back to encapsulatedApi for spine contracts that use
248
- // direct assignment (e.g. Static contract) rather than getters
249
- if (instance.spineContractCapsuleInstances) {
250
- for (const sci of Object.values(instance.spineContractCapsuleInstances) as any[]) {
251
- if (sci.self && sci.encapsulatedApi) {
252
- for (const key of Object.keys(sci.encapsulatedApi)) {
253
- if (key in sci.self && sci.encapsulatedApi[key] !== sci.self[key]) {
254
- sci.encapsulatedApi[key] = sci.self[key]
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 StructInit functions and mapped capsule instances from all spine contract capsule instances
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
- return item && typeof item === 'object' && !Array.isArray(item)
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
- return getterFn.call(selfProxy)
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
- apiTarget[property.name] = property.definition.value.bind(selfProxy)
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
- return getterFn.call(selfProxy)
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
- return getterFn.call(selfProxy)
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