@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.
- package/README.md +21 -0
- package/package.json +21 -0
- package/src/capsule-projectors/CapsuleModuleProjector.v0.ts +1716 -0
- package/src/encapsulate.ts +662 -0
- package/src/spine-contracts/CapsuleSpineContract.v0/Membrane.v0.ts +624 -0
- package/src/spine-contracts/CapsuleSpineContract.v0/README.md +28 -0
- package/src/spine-contracts/CapsuleSpineContract.v0/Static.v0.ts +290 -0
- package/src/spine-factories/CapsuleSpineFactory.v0.ts +299 -0
- package/src/spine-factories/TimingObserver.ts +26 -0
- package/src/static-analyzer.v0.ts +1591 -0
- package/structs/Capsule.v0.ts +22 -0
- package/tsconfig.json +30 -0
|
@@ -0,0 +1,624 @@
|
|
|
1
|
+
import { CapsulePropertyTypes } from "../../encapsulate"
|
|
2
|
+
import { ContractCapsuleInstanceFactory } from "./Static.v0"
|
|
3
|
+
|
|
4
|
+
function parseCallerFromStack(stack: string, spineFilesystemRoot?: string): Array<{ function?: string, filepath?: string, line?: number, column?: number }> {
|
|
5
|
+
const lines = stack.split('\n')
|
|
6
|
+
const result: Array<{ function?: string, filepath?: string, line?: number, column?: number }> = []
|
|
7
|
+
|
|
8
|
+
// Skip first line (Error message), then collect ALL frames
|
|
9
|
+
for (let i = 1; i < lines.length; i++) {
|
|
10
|
+
const line = lines[i].trim()
|
|
11
|
+
|
|
12
|
+
// Match various stack trace formats:
|
|
13
|
+
// "at functionName (/path/to/file:line:column)"
|
|
14
|
+
// "at /path/to/file:line:column"
|
|
15
|
+
// "at functionName (file:line:column)"
|
|
16
|
+
const match = line.match(/at\s+(.+)/)
|
|
17
|
+
if (match) {
|
|
18
|
+
const frame: { function?: string, filepath?: string, line?: number, column?: number } = {}
|
|
19
|
+
const content = match[1]
|
|
20
|
+
|
|
21
|
+
// Try to extract function name and location
|
|
22
|
+
const funcMatch = content.match(/^(.+?)\s+\((.+)\)$/)
|
|
23
|
+
if (funcMatch) {
|
|
24
|
+
// Has function name: "functionName (/path/to/file:line:column)"
|
|
25
|
+
const funcName = funcMatch[1].trim()
|
|
26
|
+
// Only include function name if not anonymous
|
|
27
|
+
if (funcName !== '<anonymous>' && funcName !== 'async <anonymous>') {
|
|
28
|
+
frame.function = funcName
|
|
29
|
+
}
|
|
30
|
+
const location = funcMatch[2]
|
|
31
|
+
const locMatch = location.match(/^(.+):(\d+):(\d+)$/)
|
|
32
|
+
if (locMatch) {
|
|
33
|
+
frame.filepath = locMatch[1]
|
|
34
|
+
frame.line = parseInt(locMatch[2], 10)
|
|
35
|
+
frame.column = parseInt(locMatch[3], 10)
|
|
36
|
+
}
|
|
37
|
+
} else {
|
|
38
|
+
// No function name: "/path/to/file:line:column"
|
|
39
|
+
const locMatch = content.match(/^(.+):(\d+):(\d+)$/)
|
|
40
|
+
if (locMatch) {
|
|
41
|
+
frame.filepath = locMatch[1]
|
|
42
|
+
frame.line = parseInt(locMatch[2], 10)
|
|
43
|
+
frame.column = parseInt(locMatch[3], 10)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Convert absolute paths to relative paths if spineFilesystemRoot is provided
|
|
48
|
+
if (frame.filepath && spineFilesystemRoot) {
|
|
49
|
+
if (frame.filepath.startsWith(spineFilesystemRoot)) {
|
|
50
|
+
frame.filepath = frame.filepath.slice(spineFilesystemRoot.length)
|
|
51
|
+
// Remove leading slash if present
|
|
52
|
+
if (frame.filepath.startsWith('/')) {
|
|
53
|
+
frame.filepath = frame.filepath.slice(1)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Include all frames, even if incomplete
|
|
59
|
+
if (frame.filepath || frame.function) {
|
|
60
|
+
result.push(frame)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return result
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function extractCallerInfo(stack: Array<{ function?: string, filepath?: string, line?: number, column?: number }>, offset: number = 0) {
|
|
68
|
+
// Use offset to skip frames in the stack
|
|
69
|
+
// offset 0 = first frame, offset 1 = second frame, etc.
|
|
70
|
+
|
|
71
|
+
if (offset < stack.length) {
|
|
72
|
+
const frame = stack[offset]
|
|
73
|
+
return {
|
|
74
|
+
filepath: frame.filepath,
|
|
75
|
+
line: frame.line
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Fallback to first frame if offset is out of bounds
|
|
80
|
+
if (stack.length > 0) {
|
|
81
|
+
return {
|
|
82
|
+
filepath: stack[0].filepath,
|
|
83
|
+
line: stack[0].line
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return {}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
type CallerContext = {
|
|
90
|
+
capsuleSourceLineRef: string
|
|
91
|
+
capsuleSourceNameRef?: string
|
|
92
|
+
spineContractCapsuleInstanceId: string
|
|
93
|
+
capsuleSourceNameRefHash?: string
|
|
94
|
+
prop?: string
|
|
95
|
+
filepath?: string
|
|
96
|
+
line?: number
|
|
97
|
+
stack?: Array<{ function?: string, filepath?: string, line?: number, column?: number }>
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function CapsuleMembrane(target: Record<string, any>, hooks?: {
|
|
101
|
+
onGet?: (data: { prop: string, value: any }) => void
|
|
102
|
+
onSet?: (data: { prop: string, value: any }) => void
|
|
103
|
+
onBeforeCall?: (data: { prop: string, args: any[] }) => void
|
|
104
|
+
onAfterCall?: (data: { prop: string, result: any, args: any[] }) => void
|
|
105
|
+
}, callerContext?: CallerContext) {
|
|
106
|
+
return new Proxy(target, {
|
|
107
|
+
get(obj: any, prop: string | symbol) {
|
|
108
|
+
if (typeof prop === 'symbol') return obj[prop]
|
|
109
|
+
|
|
110
|
+
const value = obj[prop]
|
|
111
|
+
hooks?.onGet?.({ prop: prop as string, value })
|
|
112
|
+
|
|
113
|
+
if (typeof value === 'function') {
|
|
114
|
+
return function (this: any, ...args: any[]) {
|
|
115
|
+
hooks?.onBeforeCall?.({ prop: prop as string, args })
|
|
116
|
+
const result = value.apply(this, args)
|
|
117
|
+
hooks?.onAfterCall?.({ prop: prop as string, args, result })
|
|
118
|
+
return result
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return value
|
|
123
|
+
},
|
|
124
|
+
set(obj: any, prop: string | symbol, value: any) {
|
|
125
|
+
if (typeof prop === 'symbol') {
|
|
126
|
+
obj[prop] = value
|
|
127
|
+
return true
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
hooks?.onSet?.({ prop: prop as string, value })
|
|
131
|
+
obj[prop] = value
|
|
132
|
+
return true
|
|
133
|
+
}
|
|
134
|
+
})
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class MembraneContractCapsuleInstanceFactory extends ContractCapsuleInstanceFactory {
|
|
139
|
+
private getEventIndex: () => number
|
|
140
|
+
private incrementEventIndex: () => number
|
|
141
|
+
private currentCallerContext: CallerContext | undefined
|
|
142
|
+
private onMembraneEvent?: (event: any) => void
|
|
143
|
+
private enableCallerStackInference: boolean
|
|
144
|
+
private encapsulateOptions: any
|
|
145
|
+
private capsuleSourceNameRef?: string
|
|
146
|
+
private capsuleSourceNameRefHash?: string
|
|
147
|
+
private runtimeSpineContracts?: Record<string, any>
|
|
148
|
+
public id: string
|
|
149
|
+
|
|
150
|
+
constructor({
|
|
151
|
+
spineContractUri,
|
|
152
|
+
capsule,
|
|
153
|
+
self,
|
|
154
|
+
encapsulatedApi,
|
|
155
|
+
resolve,
|
|
156
|
+
importCapsule,
|
|
157
|
+
spineFilesystemRoot,
|
|
158
|
+
freezeCapsule,
|
|
159
|
+
onMembraneEvent,
|
|
160
|
+
enableCallerStackInference,
|
|
161
|
+
encapsulateOptions,
|
|
162
|
+
getEventIndex,
|
|
163
|
+
incrementEventIndex,
|
|
164
|
+
currentCallerContext,
|
|
165
|
+
runtimeSpineContracts
|
|
166
|
+
}: {
|
|
167
|
+
spineContractUri: string
|
|
168
|
+
capsule: any
|
|
169
|
+
self: any
|
|
170
|
+
encapsulatedApi: Record<string, any>
|
|
171
|
+
resolve?: (uri: string, parentFilepath: string) => Promise<string>
|
|
172
|
+
importCapsule?: (filepath: string) => Promise<any>
|
|
173
|
+
spineFilesystemRoot?: string
|
|
174
|
+
freezeCapsule?: (capsule: any) => Promise<any>
|
|
175
|
+
onMembraneEvent?: (event: any) => void
|
|
176
|
+
enableCallerStackInference: boolean
|
|
177
|
+
encapsulateOptions: any
|
|
178
|
+
getEventIndex: () => number
|
|
179
|
+
incrementEventIndex: () => number
|
|
180
|
+
currentCallerContext?: CallerContext
|
|
181
|
+
runtimeSpineContracts?: Record<string, any>
|
|
182
|
+
}) {
|
|
183
|
+
super({ spineContractUri, capsule, self, encapsulatedApi, resolve, importCapsule, spineFilesystemRoot, freezeCapsule })
|
|
184
|
+
this.getEventIndex = getEventIndex
|
|
185
|
+
this.incrementEventIndex = incrementEventIndex
|
|
186
|
+
this.currentCallerContext = currentCallerContext
|
|
187
|
+
this.onMembraneEvent = onMembraneEvent
|
|
188
|
+
this.enableCallerStackInference = enableCallerStackInference
|
|
189
|
+
this.encapsulateOptions = encapsulateOptions
|
|
190
|
+
this.capsuleSourceNameRef = capsule?.cst?.capsuleSourceNameRef
|
|
191
|
+
this.capsuleSourceNameRefHash = capsule?.cst?.capsuleSourceNameRefHash
|
|
192
|
+
this.runtimeSpineContracts = runtimeSpineContracts
|
|
193
|
+
this.id = `$${encapsulateOptions.capsuleSourceLineRef}`
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
setCurrentCallerContext(context: CallerContext | undefined): void {
|
|
197
|
+
this.currentCallerContext = context
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
protected async mapMappingProperty({ overrides, options, property }: { overrides: any, options: any, property: any }) {
|
|
201
|
+
|
|
202
|
+
const mappedCapsule = await this.resolveMappedCapsule({ property })
|
|
203
|
+
const constants = await this.extractConstants({ mappedCapsule })
|
|
204
|
+
|
|
205
|
+
const mappingOptions = await property.definition.options?.({
|
|
206
|
+
constants
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
// Transform overrides if this mapping has a propertyContractDelegate
|
|
210
|
+
let mappedOverrides = overrides
|
|
211
|
+
if (property.definition.propertyContractDelegate) {
|
|
212
|
+
|
|
213
|
+
// Extract overrides for the delegate property contract and map them to '#'
|
|
214
|
+
// Try both capsuleSourceLineRef and capsuleName
|
|
215
|
+
const delegateOverrides =
|
|
216
|
+
overrides?.[this.capsule.encapsulateOptions.capsuleSourceLineRef]?.[property.definition.propertyContractDelegate] ||
|
|
217
|
+
(this.capsule.encapsulateOptions.capsuleName && overrides?.[this.capsule.encapsulateOptions.capsuleName]?.[property.definition.propertyContractDelegate])
|
|
218
|
+
|
|
219
|
+
if (delegateOverrides) {
|
|
220
|
+
mappedOverrides = {
|
|
221
|
+
...overrides,
|
|
222
|
+
[mappedCapsule.capsuleSourceLineRef]: {
|
|
223
|
+
'#': delegateOverrides
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
if (mappedCapsule.encapsulateOptions.capsuleName) {
|
|
227
|
+
mappedOverrides[mappedCapsule.encapsulateOptions.capsuleName] = {
|
|
228
|
+
'#': delegateOverrides
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const mappedCapsuleInstance = await mappedCapsule.makeInstance({
|
|
235
|
+
overrides: mappedOverrides,
|
|
236
|
+
options: mappingOptions,
|
|
237
|
+
runtimeSpineContracts: this.runtimeSpineContracts
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
const apiTarget = this.getApiTarget({ property })
|
|
241
|
+
apiTarget[property.name] = new Proxy(mappedCapsuleInstance, {
|
|
242
|
+
get: (apiTarget: any, apiProp: string | symbol) => {
|
|
243
|
+
if (typeof apiProp === 'symbol') return apiTarget[apiProp]
|
|
244
|
+
|
|
245
|
+
this.currentCallerContext = {
|
|
246
|
+
capsuleSourceLineRef: this.encapsulateOptions.capsuleSourceLineRef,
|
|
247
|
+
capsuleSourceNameRef: this.capsuleSourceNameRef,
|
|
248
|
+
spineContractCapsuleInstanceId: this.id,
|
|
249
|
+
capsuleSourceNameRefHash: this.capsuleSourceNameRefHash,
|
|
250
|
+
prop: apiProp as string
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (this.enableCallerStackInference) {
|
|
254
|
+
const stackStr = new Error('[MAPPED_CAPSULE]').stack
|
|
255
|
+
if (stackStr) {
|
|
256
|
+
const stackFrames = parseCallerFromStack(stackStr, this.spineFilesystemRoot)
|
|
257
|
+
if (stackFrames.length > 0) {
|
|
258
|
+
const callerInfo = extractCallerInfo(stackFrames, 3)
|
|
259
|
+
this.currentCallerContext.filepath = callerInfo.filepath
|
|
260
|
+
this.currentCallerContext.line = callerInfo.line
|
|
261
|
+
this.currentCallerContext.stack = stackFrames
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return apiTarget[apiProp]
|
|
267
|
+
}
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
// Wrap unwrapped API in membrane proxy for this.self
|
|
271
|
+
this.self[property.name] = mappedCapsuleInstance.api ? new Proxy(mappedCapsuleInstance.api, {
|
|
272
|
+
get: (target, prop) => {
|
|
273
|
+
if (typeof prop === 'symbol') return target[prop]
|
|
274
|
+
|
|
275
|
+
const value = target[prop]
|
|
276
|
+
// Recursively unwrap nested .api objects
|
|
277
|
+
if (value && typeof value === 'object' && value.api) {
|
|
278
|
+
return value.api
|
|
279
|
+
}
|
|
280
|
+
return value
|
|
281
|
+
}
|
|
282
|
+
}) : mappedCapsuleInstance
|
|
283
|
+
|
|
284
|
+
// If this mapping has a propertyContractDelegate, also mount the mapped capsule's properties
|
|
285
|
+
// to the property contract namespace for direct access
|
|
286
|
+
if (property.definition.propertyContractDelegate) {
|
|
287
|
+
// Create the property contract namespace if it doesn't exist
|
|
288
|
+
if (!this.encapsulatedApi[property.definition.propertyContractDelegate]) {
|
|
289
|
+
this.encapsulatedApi[property.definition.propertyContractDelegate] = {}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Get property definitions from the mapped capsule's CST instead of accessing .api
|
|
293
|
+
// This avoids triggering the proxy and firing unwanted membrane events
|
|
294
|
+
const delegateTarget = this.encapsulatedApi[property.definition.propertyContractDelegate]
|
|
295
|
+
const mappedCapsuleCst = mappedCapsule.cst
|
|
296
|
+
const spineContractProperties = mappedCapsuleCst?.spineContracts?.[this.spineContractUri]?.properties
|
|
297
|
+
|
|
298
|
+
if (spineContractProperties) {
|
|
299
|
+
for (const [key, propDef] of Object.entries(spineContractProperties)) {
|
|
300
|
+
// Skip internal properties that start with '#'
|
|
301
|
+
if (key.startsWith('#')) continue
|
|
302
|
+
|
|
303
|
+
// Wrap the property access in a proxy to track membrane events
|
|
304
|
+
Object.defineProperty(delegateTarget, key, {
|
|
305
|
+
get: () => {
|
|
306
|
+
this.currentCallerContext = {
|
|
307
|
+
capsuleSourceLineRef: this.encapsulateOptions.capsuleSourceLineRef,
|
|
308
|
+
capsuleSourceNameRef: this.capsuleSourceNameRef,
|
|
309
|
+
spineContractCapsuleInstanceId: this.id,
|
|
310
|
+
capsuleSourceNameRefHash: this.capsuleSourceNameRefHash,
|
|
311
|
+
prop: key
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (this.enableCallerStackInference) {
|
|
315
|
+
const stackStr = new Error('[PROPERTY_CONTRACT_DELEGATE]').stack
|
|
316
|
+
if (stackStr) {
|
|
317
|
+
const stackFrames = parseCallerFromStack(stackStr, this.spineFilesystemRoot)
|
|
318
|
+
if (stackFrames.length > 0) {
|
|
319
|
+
const callerInfo = extractCallerInfo(stackFrames, 3)
|
|
320
|
+
this.currentCallerContext.filepath = callerInfo.filepath
|
|
321
|
+
this.currentCallerContext.line = callerInfo.line
|
|
322
|
+
this.currentCallerContext.stack = stackFrames
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Access the actual value from the instance's api
|
|
328
|
+
return mappedCapsuleInstance.api[key]
|
|
329
|
+
},
|
|
330
|
+
enumerable: true,
|
|
331
|
+
configurable: true
|
|
332
|
+
})
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
protected mapLiteralProperty({ property }: { property: any }) {
|
|
339
|
+
|
|
340
|
+
const value = typeof this.self[property.name] !== 'undefined'
|
|
341
|
+
? this.self[property.name]
|
|
342
|
+
: property.definition.value
|
|
343
|
+
|
|
344
|
+
const valueKey = `__value_${property.name}`
|
|
345
|
+
Object.defineProperty(this.encapsulatedApi, valueKey, {
|
|
346
|
+
value: value,
|
|
347
|
+
writable: true,
|
|
348
|
+
enumerable: false,
|
|
349
|
+
configurable: true
|
|
350
|
+
})
|
|
351
|
+
|
|
352
|
+
Object.defineProperty(this.encapsulatedApi, property.name, {
|
|
353
|
+
get: () => {
|
|
354
|
+
const event: any = {
|
|
355
|
+
event: 'get',
|
|
356
|
+
eventIndex: this.incrementEventIndex(),
|
|
357
|
+
target: {
|
|
358
|
+
capsuleSourceLineRef: this.encapsulateOptions.capsuleSourceLineRef,
|
|
359
|
+
spineContractCapsuleInstanceId: this.id,
|
|
360
|
+
prop: property.name,
|
|
361
|
+
},
|
|
362
|
+
value: this.encapsulatedApi[valueKey]
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (this.capsuleSourceNameRef) {
|
|
366
|
+
event.target.capsuleSourceNameRef = this.capsuleSourceNameRef
|
|
367
|
+
}
|
|
368
|
+
if (this.capsuleSourceNameRefHash) {
|
|
369
|
+
event.target.capsuleSourceNameRefHash = this.capsuleSourceNameRefHash
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
this.addCallerContextToEvent(event)
|
|
373
|
+
this.onMembraneEvent?.(event)
|
|
374
|
+
return this.encapsulatedApi[valueKey]
|
|
375
|
+
},
|
|
376
|
+
set: (newValue) => {
|
|
377
|
+
const event: any = {
|
|
378
|
+
event: 'set',
|
|
379
|
+
eventIndex: this.incrementEventIndex(),
|
|
380
|
+
target: {
|
|
381
|
+
capsuleSourceLineRef: this.encapsulateOptions.capsuleSourceLineRef,
|
|
382
|
+
spineContractCapsuleInstanceId: this.id,
|
|
383
|
+
prop: property.name,
|
|
384
|
+
},
|
|
385
|
+
value: newValue
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (this.capsuleSourceNameRef) {
|
|
389
|
+
event.target.capsuleSourceNameRef = this.capsuleSourceNameRef
|
|
390
|
+
}
|
|
391
|
+
if (this.capsuleSourceNameRefHash) {
|
|
392
|
+
event.target.capsuleSourceNameRefHash = this.capsuleSourceNameRefHash
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
this.addCallerContextToEvent(event)
|
|
396
|
+
this.onMembraneEvent?.(event)
|
|
397
|
+
this.encapsulatedApi[valueKey] = newValue
|
|
398
|
+
},
|
|
399
|
+
enumerable: true,
|
|
400
|
+
configurable: true
|
|
401
|
+
})
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
protected mapFunctionProperty({ property }: { property: any }) {
|
|
405
|
+
|
|
406
|
+
// Create a proxy for this.self that intercepts property access
|
|
407
|
+
// Prefer this.self (which has unwrapped APIs) over encapsulatedApi
|
|
408
|
+
const selfProxy = new Proxy(this.self, {
|
|
409
|
+
get: (target: any, prop: string | symbol) => {
|
|
410
|
+
if (typeof prop === 'symbol') return target[prop]
|
|
411
|
+
|
|
412
|
+
// First check if the property exists in target (this.self)
|
|
413
|
+
if (prop in target) {
|
|
414
|
+
return target[prop]
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Fall back to encapsulatedApi
|
|
418
|
+
if (prop in this.encapsulatedApi) {
|
|
419
|
+
return this.encapsulatedApi[prop]
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
return undefined
|
|
423
|
+
}
|
|
424
|
+
})
|
|
425
|
+
|
|
426
|
+
const boundFunction = property.definition.value.bind(selfProxy)
|
|
427
|
+
|
|
428
|
+
const valueKey = `__value_${property.name}`
|
|
429
|
+
Object.defineProperty(this.encapsulatedApi, valueKey, {
|
|
430
|
+
value: boundFunction,
|
|
431
|
+
writable: true,
|
|
432
|
+
enumerable: false,
|
|
433
|
+
configurable: true
|
|
434
|
+
})
|
|
435
|
+
|
|
436
|
+
Object.defineProperty(this.encapsulatedApi, property.name, {
|
|
437
|
+
get: () => {
|
|
438
|
+
return (...args: any[]) => {
|
|
439
|
+
const callEvent: any = {
|
|
440
|
+
event: 'call',
|
|
441
|
+
eventIndex: this.incrementEventIndex(),
|
|
442
|
+
target: {
|
|
443
|
+
capsuleSourceLineRef: this.encapsulateOptions.capsuleSourceLineRef,
|
|
444
|
+
spineContractCapsuleInstanceId: this.id,
|
|
445
|
+
prop: property.name,
|
|
446
|
+
},
|
|
447
|
+
args
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
if (this.capsuleSourceNameRef) {
|
|
451
|
+
callEvent.target.capsuleSourceNameRef = this.capsuleSourceNameRef
|
|
452
|
+
}
|
|
453
|
+
if (this.capsuleSourceNameRefHash) {
|
|
454
|
+
callEvent.target.capsuleSourceNameRefHash = this.capsuleSourceNameRefHash
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
this.addCallerContextToEvent(callEvent)
|
|
458
|
+
this.onMembraneEvent?.(callEvent)
|
|
459
|
+
|
|
460
|
+
const result = boundFunction(...args)
|
|
461
|
+
|
|
462
|
+
const resultEvent: any = {
|
|
463
|
+
event: 'call-result',
|
|
464
|
+
eventIndex: this.incrementEventIndex(),
|
|
465
|
+
callEventIndex: callEvent.eventIndex,
|
|
466
|
+
target: {
|
|
467
|
+
spineContractCapsuleInstanceId: this.id,
|
|
468
|
+
},
|
|
469
|
+
result
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
this.onMembraneEvent?.(resultEvent)
|
|
473
|
+
|
|
474
|
+
return result
|
|
475
|
+
}
|
|
476
|
+
},
|
|
477
|
+
enumerable: true,
|
|
478
|
+
configurable: true
|
|
479
|
+
})
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
protected mapGetterFunctionProperty({ property }: { property: any }) {
|
|
483
|
+
const getterFn = property.definition.value
|
|
484
|
+
|
|
485
|
+
// Create a proxy for this.self that intercepts property access
|
|
486
|
+
// Prefer this.self (which has unwrapped APIs) over encapsulatedApi
|
|
487
|
+
const selfProxy = new Proxy(this.self, {
|
|
488
|
+
get: (target: any, prop: string | symbol) => {
|
|
489
|
+
if (typeof prop === 'symbol') return target[prop]
|
|
490
|
+
|
|
491
|
+
// First check if the property exists in target (this.self)
|
|
492
|
+
if (prop in target) {
|
|
493
|
+
return target[prop]
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Fall back to encapsulatedApi
|
|
497
|
+
if (prop in this.encapsulatedApi) {
|
|
498
|
+
return this.encapsulatedApi[prop]
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
return undefined
|
|
502
|
+
}
|
|
503
|
+
})
|
|
504
|
+
|
|
505
|
+
Object.defineProperty(this.encapsulatedApi, property.name, {
|
|
506
|
+
get: () => {
|
|
507
|
+
// Call the getter function lazily when accessed with proper this context
|
|
508
|
+
const result = getterFn.call(selfProxy)
|
|
509
|
+
|
|
510
|
+
const event: any = {
|
|
511
|
+
event: 'get',
|
|
512
|
+
eventIndex: this.incrementEventIndex(),
|
|
513
|
+
target: {
|
|
514
|
+
capsuleSourceLineRef: this.encapsulateOptions.capsuleSourceLineRef,
|
|
515
|
+
spineContractCapsuleInstanceId: this.id,
|
|
516
|
+
prop: property.name,
|
|
517
|
+
},
|
|
518
|
+
value: result
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
if (this.capsuleSourceNameRef) {
|
|
522
|
+
event.target.capsuleSourceNameRef = this.capsuleSourceNameRef
|
|
523
|
+
}
|
|
524
|
+
if (this.capsuleSourceNameRefHash) {
|
|
525
|
+
event.target.capsuleSourceNameRefHash = this.capsuleSourceNameRefHash
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
this.addCallerContextToEvent(event)
|
|
529
|
+
this.onMembraneEvent?.(event)
|
|
530
|
+
return result
|
|
531
|
+
},
|
|
532
|
+
enumerable: true,
|
|
533
|
+
configurable: true
|
|
534
|
+
})
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
private addCallerContextToEvent(event: any): void {
|
|
538
|
+
if (this.currentCallerContext) {
|
|
539
|
+
event.caller = {
|
|
540
|
+
capsuleSourceLineRef: this.currentCallerContext.capsuleSourceLineRef,
|
|
541
|
+
spineContractCapsuleInstanceId: this.currentCallerContext.spineContractCapsuleInstanceId,
|
|
542
|
+
}
|
|
543
|
+
if (this.currentCallerContext.capsuleSourceNameRef) {
|
|
544
|
+
event.caller.capsuleSourceNameRef = this.currentCallerContext.capsuleSourceNameRef
|
|
545
|
+
}
|
|
546
|
+
if (this.currentCallerContext.capsuleSourceNameRefHash) {
|
|
547
|
+
event.caller.capsuleSourceNameRefHash = this.currentCallerContext.capsuleSourceNameRefHash
|
|
548
|
+
}
|
|
549
|
+
if (this.currentCallerContext.prop) {
|
|
550
|
+
event.caller.prop = this.currentCallerContext.prop
|
|
551
|
+
}
|
|
552
|
+
if (this.currentCallerContext.filepath) {
|
|
553
|
+
event.caller.filepath = this.currentCallerContext.filepath
|
|
554
|
+
}
|
|
555
|
+
if (this.currentCallerContext.line) {
|
|
556
|
+
event.caller.line = this.currentCallerContext.line
|
|
557
|
+
}
|
|
558
|
+
if (this.currentCallerContext.stack) {
|
|
559
|
+
event.caller.stack = this.currentCallerContext.stack
|
|
560
|
+
}
|
|
561
|
+
} else if (this.enableCallerStackInference) {
|
|
562
|
+
const stackStr = new Error('[MEMBRANE_EVENT]').stack
|
|
563
|
+
if (stackStr) {
|
|
564
|
+
const stackFrames = parseCallerFromStack(stackStr, this.spineFilesystemRoot)
|
|
565
|
+
if (stackFrames.length > 0) {
|
|
566
|
+
const callerInfo = extractCallerInfo(stackFrames, 3)
|
|
567
|
+
event.caller = {
|
|
568
|
+
...callerInfo,
|
|
569
|
+
stack: stackFrames
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
export function CapsuleSpineContract({
|
|
578
|
+
onMembraneEvent,
|
|
579
|
+
freezeCapsule,
|
|
580
|
+
enableCallerStackInference = false,
|
|
581
|
+
spineFilesystemRoot,
|
|
582
|
+
resolve,
|
|
583
|
+
importCapsule
|
|
584
|
+
}: {
|
|
585
|
+
onMembraneEvent?: (event: any) => void
|
|
586
|
+
freezeCapsule?: (capsule: any) => Promise<any>
|
|
587
|
+
enableCallerStackInference?: boolean
|
|
588
|
+
spineFilesystemRoot?: string
|
|
589
|
+
resolve?: (uri: string, parentFilepath: string) => Promise<string>
|
|
590
|
+
importCapsule?: (filepath: string) => Promise<any>
|
|
591
|
+
} = {}) {
|
|
592
|
+
|
|
593
|
+
let eventIndex = 0
|
|
594
|
+
let currentCallerContext: CallerContext | undefined = undefined
|
|
595
|
+
|
|
596
|
+
return {
|
|
597
|
+
'#': CapsuleSpineContract['#'],
|
|
598
|
+
makeContractCapsuleInstance: ({ encapsulateOptions, spineContractUri, self, capsule, encapsulatedApi, runtimeSpineContracts }: { encapsulateOptions: any, spineContractUri: string, self: any, capsule?: any, encapsulatedApi: Record<string, any>, runtimeSpineContracts?: Record<string, any> }) => {
|
|
599
|
+
return new MembraneContractCapsuleInstanceFactory({
|
|
600
|
+
spineContractUri,
|
|
601
|
+
capsule,
|
|
602
|
+
self,
|
|
603
|
+
encapsulatedApi,
|
|
604
|
+
spineFilesystemRoot,
|
|
605
|
+
freezeCapsule,
|
|
606
|
+
resolve,
|
|
607
|
+
importCapsule,
|
|
608
|
+
onMembraneEvent,
|
|
609
|
+
enableCallerStackInference,
|
|
610
|
+
encapsulateOptions,
|
|
611
|
+
getEventIndex: () => eventIndex,
|
|
612
|
+
incrementEventIndex: () => eventIndex++,
|
|
613
|
+
currentCallerContext,
|
|
614
|
+
runtimeSpineContracts
|
|
615
|
+
})
|
|
616
|
+
},
|
|
617
|
+
hydrate: ({ capsuleSnapshot }: { capsuleSnapshot: any }): any => {
|
|
618
|
+
return capsuleSnapshot
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
CapsuleSpineContract['#'] = '@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0'
|
|
624
|
+
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
|
|
2
|
+
Capsule Spine Contract v0
|
|
3
|
+
===
|
|
4
|
+
|
|
5
|
+
The default spine contract used to incubate spine functionality.
|
|
6
|
+
|
|
7
|
+
A spine contract is a standard and implementation that governs:
|
|
8
|
+
|
|
9
|
+
- how capsule properties are mapped to the encapsulated api and
|
|
10
|
+
- which features are available to bind additional logic controlled by definitions declared in the capsule source
|
|
11
|
+
|
|
12
|
+
Spine contracts define ecosystems as source code is written against these standards. They define the fundamental logic of how components and their internal APIs are bound.
|
|
13
|
+
|
|
14
|
+
In practice there should only ever be very few spine contracts but there can be a plethora of different partial or full implementations of the same standard.
|
|
15
|
+
|
|
16
|
+
This spine contract aims to realize a concrete implementation of the [PrivateData.Space](https://privatedata.space/) model for the purpose of building full-stack distributed applications & systems.
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
Example Capsule Source
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
Reference
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
|