@stream44.studio/encapsulate 0.4.0-rc.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,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 JavaScript applications & systems.
17
+
18
+
19
+ Example Capsule Source
20
+ ---
21
+
22
+
23
+
24
+
25
+ Reference
26
+ ---
27
+
28
+
@@ -0,0 +1,395 @@
1
+ import { CapsulePropertyTypes, join } from "../../encapsulate"
2
+
3
+ // Type for capsule instance registry - scoped per spine contract instance
4
+ export type CapsuleInstanceRegistry = Map<string, any>
5
+
6
+ export class ContractCapsuleInstanceFactory {
7
+
8
+ protected spineContractUri: string
9
+ protected capsule: any
10
+ protected self: any
11
+ protected encapsulatedApi: Record<string, any>
12
+ protected resolve?: (uri: string, parentFilepath: string) => Promise<string>
13
+ protected importCapsule?: (filepath: string) => Promise<any>
14
+ protected spineFilesystemRoot?: string
15
+ protected freezeCapsule?: (capsule: any) => Promise<any>
16
+ protected instanceRegistry?: CapsuleInstanceRegistry
17
+ protected extendedCapsuleInstance?: any
18
+ protected ownSelf?: any
19
+ protected runtimeSpineContracts?: Record<string, any>
20
+ public structInitFunctions: Array<() => any> = []
21
+ public mappedCapsuleInstances: Array<any> = []
22
+
23
+ constructor({ spineContractUri, capsule, self, ownSelf, encapsulatedApi, resolve, importCapsule, spineFilesystemRoot, freezeCapsule, instanceRegistry, extendedCapsuleInstance, runtimeSpineContracts }: { 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> }) {
24
+ this.spineContractUri = spineContractUri
25
+ this.capsule = capsule
26
+ this.self = self
27
+ this.ownSelf = ownSelf
28
+ this.encapsulatedApi = encapsulatedApi
29
+ this.resolve = resolve
30
+ this.importCapsule = importCapsule
31
+ this.spineFilesystemRoot = spineFilesystemRoot
32
+ this.freezeCapsule = freezeCapsule
33
+ this.instanceRegistry = instanceRegistry
34
+ this.extendedCapsuleInstance = extendedCapsuleInstance
35
+ this.runtimeSpineContracts = runtimeSpineContracts
36
+ }
37
+
38
+ async mapProperty({ overrides, options, property }: { overrides: any, options: any, property: any }) {
39
+ if (property.definition.type === CapsulePropertyTypes.Mapping) {
40
+ await this.mapMappingProperty({ overrides, options, property })
41
+ } else if (
42
+ property.definition.type === CapsulePropertyTypes.String ||
43
+ property.definition.type === CapsulePropertyTypes.Literal
44
+ ) {
45
+ this.mapLiteralProperty({ property })
46
+ } else if (property.definition.type === CapsulePropertyTypes.Function) {
47
+ this.mapFunctionProperty({ property })
48
+ } else if (property.definition.type === CapsulePropertyTypes.GetterFunction) {
49
+ this.mapGetterFunctionProperty({ property })
50
+ } else if (property.definition.type === CapsulePropertyTypes.StructInit) {
51
+ this.mapStructInitProperty({ property })
52
+ }
53
+ }
54
+
55
+ protected getApiTarget({ property }: { property: any }) {
56
+ // Properties under '#' go directly on the API
57
+ // Properties under '#<uri>' go under api['#<uri>']
58
+ if (!property.propertyContractUri || property.propertyContractUri === '#') {
59
+ return this.encapsulatedApi
60
+ } else {
61
+ // Namespace under the property contract key
62
+ if (!this.encapsulatedApi[property.propertyContractUri]) {
63
+ this.encapsulatedApi[property.propertyContractUri] = {}
64
+ }
65
+ return this.encapsulatedApi[property.propertyContractUri]
66
+ }
67
+ }
68
+
69
+ protected async resolveMappedCapsule({ property }: { property: any }) {
70
+ let mappedCapsule
71
+
72
+ if (typeof property.definition.value === 'string') {
73
+ if (!this.resolve) throw new Error(`'resolve' not set!`)
74
+ if (!this.spineFilesystemRoot) throw new Error(`'spineFilesystemRoot' not set!`)
75
+ if (!this.importCapsule) throw new Error(`'importCapsule' not set!`)
76
+
77
+ // Use encapsulateOptions.moduleFilepath (always available) instead of cst.source.moduleFilepath
78
+ const moduleFilepath = this.capsule.encapsulateOptions?.moduleFilepath || this.capsule.cst?.source?.moduleFilepath
79
+ if (!moduleFilepath) throw new Error(`'moduleFilepath' not available on capsule!`)
80
+
81
+ const parentPath = join(this.spineFilesystemRoot, moduleFilepath)
82
+ const filepath = await this.resolve(property.definition.value, parentPath)
83
+ mappedCapsule = await this.importCapsule(filepath)
84
+ } else if (
85
+ typeof property.definition.value === 'object' &&
86
+ typeof property.definition.value.capsuleSourceLineRef === 'string'
87
+ ) {
88
+ mappedCapsule = property.definition.value
89
+ } else {
90
+ throw new Error(`Unknown mapping value for property '${property.name}'!`)
91
+ }
92
+
93
+ return mappedCapsule
94
+ }
95
+
96
+ protected async extractConstants({ mappedCapsule }: { mappedCapsule: any }) {
97
+ const constants: Record<string, any> = {}
98
+
99
+ const spineContractDef = mappedCapsule.definition[this.spineContractUri]
100
+
101
+ if (!spineContractDef) {
102
+ throw new Error(`Spine contract definition not found for URI: ${this.spineContractUri}. Available keys: ${Object.keys(mappedCapsule.definition).join(', ')}`)
103
+ }
104
+
105
+ // Iterate through all keys in the spine contract definition
106
+ for (const [key, value] of Object.entries(spineContractDef)) {
107
+ if (key.startsWith('#')) {
108
+ // This is a property contract - iterate through its properties
109
+ for (const [prop, propDef] of Object.entries(value as Record<string, any>)) {
110
+ const { type, value: propValue } = propDef as any
111
+
112
+ if (typeof propValue === 'undefined') continue
113
+
114
+ if (
115
+ type === CapsulePropertyTypes.String ||
116
+ type === CapsulePropertyTypes.Literal
117
+ ) {
118
+ constants[prop] = propValue
119
+ }
120
+ }
121
+ } else {
122
+ // Regular property (backwards compatibility)
123
+ const { type, value: propValue } = value as any
124
+
125
+ if (typeof propValue === 'undefined') continue
126
+
127
+ if (
128
+ type === CapsulePropertyTypes.String ||
129
+ type === CapsulePropertyTypes.Literal
130
+ ) {
131
+ constants[key] = propValue
132
+ }
133
+ }
134
+ }
135
+
136
+ return constants
137
+ }
138
+
139
+ protected async mapMappingProperty({ overrides, options, property }: { overrides: any, options: any, property: any }) {
140
+ const mappedCapsule = await this.resolveMappedCapsule({ property })
141
+ const constants = await this.extractConstants({ mappedCapsule })
142
+
143
+ // delegateOptions is set by encapsulate.ts for property contract delegates
144
+ // options can be a function or an object for regular mappings
145
+ const mappingOptions = property.definition.delegateOptions
146
+ || (typeof property.definition.options === 'function'
147
+ ? await property.definition.options({ constants })
148
+ : property.definition.options)
149
+
150
+ // Check for existing instance in registry - reuse if available when no options
151
+ // Pre-registration with null allows parent capsules to "claim" a slot before child capsules process
152
+ const capsuleName = mappedCapsule.encapsulateOptions?.capsuleName
153
+
154
+ if (capsuleName && this.instanceRegistry) {
155
+ if (this.instanceRegistry.has(capsuleName)) {
156
+ const existingEntry = this.instanceRegistry.get(capsuleName)
157
+
158
+ // Only reuse if current mapping has no options
159
+ if (!mappingOptions) {
160
+ // Use deferred proxy that resolves from registry when accessed
161
+ // Works for both null (pre-registered) and actual instances
162
+ const apiTarget = this.getApiTarget({ property })
163
+ const registry = this.instanceRegistry
164
+ apiTarget[property.name] = new Proxy({} as any, {
165
+ get: (_target: any, apiProp: string | symbol) => {
166
+ if (typeof apiProp === 'symbol') return undefined
167
+ const resolvedInstance = registry.get(capsuleName)
168
+ if (!resolvedInstance) {
169
+ throw new Error(`Capsule instance not yet resolved: ${capsuleName}`)
170
+ }
171
+ // Access through .api if it exists (for capsule instances with getters)
172
+ if (resolvedInstance.api && apiProp in resolvedInstance.api) {
173
+ return resolvedInstance.api[apiProp]
174
+ }
175
+ return resolvedInstance[apiProp]
176
+ }
177
+ })
178
+ this.self[property.name] = new Proxy({} as any, {
179
+ get: (_target, prop) => {
180
+ if (typeof prop === 'symbol') return undefined
181
+ const resolvedInstance = registry.get(capsuleName)
182
+ if (!resolvedInstance) {
183
+ throw new Error(`Capsule instance not yet resolved: ${capsuleName}`)
184
+ }
185
+ const value = resolvedInstance.api?.[prop] ?? resolvedInstance[prop]
186
+ if (value && typeof value === 'object' && value.api) {
187
+ return value.api
188
+ }
189
+ return value
190
+ }
191
+ })
192
+ return
193
+ }
194
+ // If current mapping has options, fall through to create new instance
195
+ } else {
196
+ // Pre-register as null to claim the slot for this capsule
197
+ this.instanceRegistry.set(capsuleName, null)
198
+ }
199
+ }
200
+
201
+ // Transform overrides if this mapping has a propertyContractDelegate
202
+ let mappedOverrides = overrides
203
+ if (property.definition.propertyContractDelegate) {
204
+ // Extract overrides for the delegate property contract and map them to '#'
205
+ // Try both capsuleSourceLineRef and capsuleName
206
+ const delegateOverrides =
207
+ overrides?.[this.capsule.encapsulateOptions.capsuleSourceLineRef]?.[property.definition.propertyContractDelegate] ||
208
+ (this.capsule.encapsulateOptions.capsuleName && overrides?.[this.capsule.encapsulateOptions.capsuleName]?.[property.definition.propertyContractDelegate])
209
+
210
+ if (delegateOverrides) {
211
+ mappedOverrides = {
212
+ ...overrides,
213
+ [mappedCapsule.capsuleSourceLineRef]: {
214
+ '#': delegateOverrides
215
+ }
216
+ }
217
+ if (mappedCapsule.encapsulateOptions.capsuleName) {
218
+ mappedOverrides[mappedCapsule.encapsulateOptions.capsuleName] = {
219
+ '#': delegateOverrides
220
+ }
221
+ }
222
+ }
223
+ }
224
+
225
+ const apiTarget = this.getApiTarget({ property })
226
+ const mappedInstance = await mappedCapsule.makeInstance({
227
+ overrides: mappedOverrides,
228
+ options: mappingOptions,
229
+ runtimeSpineContracts: this.runtimeSpineContracts
230
+ })
231
+
232
+ // Register the instance (replaces null pre-registration marker)
233
+ // Always register to make instance available for child capsules with deferred proxies
234
+ if (capsuleName && this.instanceRegistry) {
235
+ this.instanceRegistry.set(capsuleName, mappedInstance)
236
+ }
237
+
238
+ apiTarget[property.name] = mappedInstance
239
+ this.mappedCapsuleInstances.push(mappedInstance)
240
+ // Use proxy to unwrap .api for this.self so internal references work
241
+ this.self[property.name] = mappedInstance.api ? new Proxy(mappedInstance.api, {
242
+ get: (target, prop) => {
243
+ const value = target[prop]
244
+ // Recursively unwrap nested .api objects
245
+ if (value && typeof value === 'object' && value.api) {
246
+ return value.api
247
+ }
248
+ return value
249
+ }
250
+ }) : mappedInstance
251
+
252
+ // If this mapping has a propertyContractDelegate, also mount the mapped capsule's API
253
+ // to the property contract namespace for direct access
254
+ if (property.definition.propertyContractDelegate) {
255
+ // Create the property contract namespace if it doesn't exist
256
+ if (!this.encapsulatedApi[property.definition.propertyContractDelegate]) {
257
+ this.encapsulatedApi[property.definition.propertyContractDelegate] = {}
258
+ }
259
+
260
+ // Mount all properties from the mapped capsule's API to the property contract namespace
261
+ const delegateTarget = this.encapsulatedApi[property.definition.propertyContractDelegate]
262
+ for (const [key, value] of Object.entries(mappedInstance.api)) {
263
+ delegateTarget[key] = value
264
+ }
265
+ }
266
+ }
267
+
268
+ protected mapLiteralProperty({ property }: { property: any }) {
269
+ const apiTarget = this.getApiTarget({ property })
270
+ // Use existing value from self if defined, otherwise use property definition
271
+ // This preserves values set by child capsules in the extends chain
272
+ const existingValue = this.self[property.name]
273
+ const value = existingValue !== undefined
274
+ ? existingValue
275
+ : property.definition.value
276
+
277
+ // Assign to both apiTarget and self so getter functions can access via this
278
+ apiTarget[property.name] = value
279
+ // Only update self if it wasn't already set (preserve child values)
280
+ if (existingValue === undefined) {
281
+ this.self[property.name] = value
282
+ }
283
+ }
284
+
285
+ protected createSelfProxy() {
286
+ const extendedApi = this.extendedCapsuleInstance?.api
287
+ const ownSelf = this.ownSelf
288
+ return new Proxy(this.self, {
289
+ get: (target: any, prop: string | symbol) => {
290
+ if (typeof prop === 'symbol') return target[prop]
291
+
292
+ // 'self' property returns ownSelf (only this capsule's own properties)
293
+ if (prop === 'self' && ownSelf) {
294
+ return ownSelf
295
+ }
296
+
297
+ // First check if the property exists in target (this.self)
298
+ if (prop in target) {
299
+ return target[prop]
300
+ }
301
+
302
+ // Fall back to encapsulatedApi
303
+ if (prop in this.encapsulatedApi) {
304
+ return this.encapsulatedApi[prop]
305
+ }
306
+
307
+ // Fall back to extended capsule's API
308
+ if (extendedApi && prop in extendedApi) {
309
+ return extendedApi[prop]
310
+ }
311
+
312
+ return undefined
313
+ }
314
+ })
315
+ }
316
+
317
+ protected mapFunctionProperty({ property }: { property: any }) {
318
+ const apiTarget = this.getApiTarget({ property })
319
+ const selfProxy = this.createSelfProxy()
320
+ apiTarget[property.name] = property.definition.value.bind(selfProxy)
321
+ }
322
+
323
+ protected mapGetterFunctionProperty({ property }: { property: any }) {
324
+ const apiTarget = this.getApiTarget({ property })
325
+ const getterFn = property.definition.value
326
+ const selfProxy = this.createSelfProxy()
327
+
328
+ // Define a lazy getter that calls the function only when accessed with proper this context
329
+ Object.defineProperty(apiTarget, property.name, {
330
+ get: () => {
331
+ return getterFn.call(selfProxy)
332
+ },
333
+ enumerable: true,
334
+ configurable: true
335
+ })
336
+
337
+ // Also define the getter on ownSelf so this.self.propertyName works for getter functions
338
+ // This ensures this.self accesses the getter, not a raw value
339
+ if (this.ownSelf) {
340
+ Object.defineProperty(this.ownSelf, property.name, {
341
+ get: () => {
342
+ return getterFn.call(selfProxy)
343
+ },
344
+ enumerable: true,
345
+ configurable: true
346
+ })
347
+ }
348
+ }
349
+
350
+ protected mapStructInitProperty({ property }: { property: any }) {
351
+ const selfProxy = this.createSelfProxy()
352
+ const boundFunction = property.definition.value.bind(selfProxy)
353
+ this.structInitFunctions.push(boundFunction)
354
+ }
355
+
356
+ async freeze(options: any): Promise<any> {
357
+ return this.freezeCapsule?.(options) || {}
358
+ }
359
+
360
+ }
361
+
362
+
363
+
364
+
365
+ export function CapsuleSpineContract({ freezeCapsule, resolve, importCapsule, spineFilesystemRoot }: { freezeCapsule?: (capsule: any) => Promise<any>, resolve?: (uri: string, parentFilepath: string) => Promise<string>, importCapsule?: (filepath: string) => Promise<any>, spineFilesystemRoot?: string } = {}) {
366
+
367
+ const instanceRegistry: CapsuleInstanceRegistry = new Map()
368
+
369
+ return {
370
+ '#': CapsuleSpineContract['#'],
371
+ instanceRegistry,
372
+ makeContractCapsuleInstance: ({ spineContractUri, capsule, self, ownSelf, encapsulatedApi, extendedCapsuleInstance, runtimeSpineContracts }: { spineContractUri: string, capsule: any, self: any, ownSelf?: any, encapsulatedApi: Record<string, any>, extendedCapsuleInstance?: any, runtimeSpineContracts?: Record<string, any> }) => {
373
+ return new ContractCapsuleInstanceFactory({
374
+ spineContractUri,
375
+ capsule,
376
+ self,
377
+ ownSelf,
378
+ encapsulatedApi,
379
+ resolve,
380
+ importCapsule,
381
+ spineFilesystemRoot,
382
+ freezeCapsule,
383
+ instanceRegistry,
384
+ extendedCapsuleInstance,
385
+ runtimeSpineContracts
386
+ })
387
+ },
388
+ hydrate: ({ capsuleSnapshot }: { capsuleSnapshot: any }): any => {
389
+
390
+ return capsuleSnapshot
391
+ }
392
+ }
393
+ }
394
+
395
+ CapsuleSpineContract['#'] = '@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0'