@stream44.studio/encapsulate 0.4.0-rc.19 → 0.4.0-rc.21
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/.o/stream44.studio/assets/Icon-v1.svg +1170 -0
- package/LICENSE.txt +1 -1
- package/README.md +23 -12
- package/package.json +1 -1
- package/src/capsule-projectors/CapsuleModuleProjector.v0.ts +19 -19
- package/src/encapsulate.ts +56 -25
- package/src/spine-contracts/CapsuleSpineContract.v0/Membrane.v0.ts +6 -4
- package/src/spine-contracts/CapsuleSpineContract.v0/Overview.drawio +261 -0
- package/src/spine-contracts/CapsuleSpineContract.v0/Overview.svg +1 -0
- package/src/spine-contracts/CapsuleSpineContract.v0/README.md +437 -1
- package/src/spine-contracts/CapsuleSpineContract.v0/Static.v0.ts +41 -3
- package/src/spine-factories/CapsuleSpineFactory.v0.ts +23 -5
- package/src/static-analyzer.v0.ts +129 -17
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
⚠️ **Disclaimer:** Under active development. Code has not been audited, APIs and interfaces are subject to change.
|
|
1
2
|
|
|
2
3
|
Capsule Spine Contract v0
|
|
3
4
|
===
|
|
4
5
|
|
|
5
|
-
The
|
|
6
|
+
The first *experimental* spine contract implementation used to incubate the spine approach.
|
|
6
7
|
|
|
7
8
|
A spine contract is a standard and implementation that governs:
|
|
8
9
|
|
|
@@ -15,14 +16,449 @@ In practice there should only ever be very few spine contracts but there can be
|
|
|
15
16
|
|
|
16
17
|
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
|
+

|
|
20
|
+
|
|
18
21
|
|
|
19
22
|
Example Capsule Source
|
|
20
23
|
---
|
|
21
24
|
|
|
25
|
+
```ts
|
|
26
|
+
// A capsule is defined by calling encapsulate() with a definition object and options.
|
|
27
|
+
// The definition is keyed by spine contract URI, then by property contract URI, then by property name.
|
|
28
|
+
|
|
29
|
+
const userService = await encapsulate({
|
|
30
|
+
'#@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0': {
|
|
31
|
+
|
|
32
|
+
// Property struct marker — attaches Capsule metadata (capsuleName, capsuleSourceLineRef, etc.)
|
|
33
|
+
'#@stream44.studio/encapsulate/structs/Capsule': {},
|
|
34
|
+
|
|
35
|
+
// External struct via property contract delegate — maps a separate capsule as a struct.
|
|
36
|
+
// 'as' aliases the full URI to a short name accessible on the API and via this.
|
|
37
|
+
// 'options' are forwarded to the struct capsule's '#' property contract as overrides.
|
|
38
|
+
'#./structs/Schema.v0': {
|
|
39
|
+
as: '$schema',
|
|
40
|
+
options: {
|
|
41
|
+
'#': { schemaValue: { type: 'user', version: 2 } }
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
// '#' is the default property contract — properties here are exposed directly on the capsule API.
|
|
46
|
+
'#': {
|
|
47
|
+
|
|
48
|
+
// --- Value Properties ---
|
|
49
|
+
|
|
50
|
+
// Literal: a value property. Accessible on the API and via this. Can be overridden at runtime.
|
|
51
|
+
name: {
|
|
52
|
+
type: CapsulePropertyTypes.Literal,
|
|
53
|
+
value: 'default-name'
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
// String: alias for Literal (semantic hint for string values).
|
|
57
|
+
label: {
|
|
58
|
+
type: CapsulePropertyTypes.String,
|
|
59
|
+
value: undefined
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
// Constant: read-only value. Throws on assignment via the Membrane contract.
|
|
63
|
+
VERSION: {
|
|
64
|
+
type: CapsulePropertyTypes.Constant,
|
|
65
|
+
value: '1.0.0'
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
// --- Function Properties ---
|
|
69
|
+
|
|
70
|
+
// Function: bound to a self proxy. Receives arguments. Callable on the API.
|
|
71
|
+
greet: {
|
|
72
|
+
type: CapsulePropertyTypes.Function,
|
|
73
|
+
value: function (this: any, greeting: string): string {
|
|
74
|
+
// 'this' is a proxy over the shared self — accesses all properties
|
|
75
|
+
// including those from extended capsules and overrides.
|
|
76
|
+
return `${greeting}, ${this.name}! (v${this.VERSION})`
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
// GetterFunction: lazily evaluated when the property is accessed (no parentheses).
|
|
81
|
+
fullLabel: {
|
|
82
|
+
type: CapsulePropertyTypes.GetterFunction,
|
|
83
|
+
value: function (this: any): string {
|
|
84
|
+
return `${this.label} [${this.name}]`
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
// SetterFunction: called when the property is assigned to. Enables validation/transformation.
|
|
89
|
+
setName: {
|
|
90
|
+
type: CapsulePropertyTypes.SetterFunction,
|
|
91
|
+
value: function (this: any, newName: string) {
|
|
92
|
+
if (!newName) throw new Error('Name cannot be empty')
|
|
93
|
+
this.name = newName.trim()
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
// Memoize: caches the return value. true = permanent, number = TTL in ms.
|
|
98
|
+
expensiveComputation: {
|
|
99
|
+
type: CapsulePropertyTypes.GetterFunction,
|
|
100
|
+
value: function (this: any): object {
|
|
101
|
+
return { computed: true, name: this.name }
|
|
102
|
+
},
|
|
103
|
+
memoize: true // cached permanently for this run
|
|
104
|
+
},
|
|
105
|
+
timedCache: {
|
|
106
|
+
type: CapsulePropertyTypes.Function,
|
|
107
|
+
value: function (this: any): object {
|
|
108
|
+
return { ts: Date.now(), name: this.name }
|
|
109
|
+
},
|
|
110
|
+
memoize: 5000 // cache expires after 5 seconds
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
// --- Mapping Properties ---
|
|
114
|
+
|
|
115
|
+
// Mapping (capsule reference): composes another capsule as a sub-component.
|
|
116
|
+
// The mapped capsule gets its own instance with its own self context.
|
|
117
|
+
$auth: {
|
|
118
|
+
type: CapsulePropertyTypes.Mapping,
|
|
119
|
+
value: authCapsule, // direct capsule reference
|
|
120
|
+
options: { // static options object forwarded to the mapped capsule
|
|
121
|
+
'#': { realm: 'users' }
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
// Mapping (string URI): resolved relative to this capsule's module filepath.
|
|
126
|
+
$db: {
|
|
127
|
+
type: CapsulePropertyTypes.Mapping,
|
|
128
|
+
value: './Database.v0', // resolved via spine contract's resolve + importCapsule
|
|
129
|
+
options: async ({ self, constants }: { self: any, constants: any }) => {
|
|
130
|
+
// Dynamic options function — receives { self, constants }.
|
|
131
|
+
// 'constants' contains Literal/String values from the mapped capsule.
|
|
132
|
+
// 'self' contains resolved sibling mappings when depends is declared.
|
|
133
|
+
return {
|
|
134
|
+
'#': { connectionString: `db://${constants.dbName}` },
|
|
135
|
+
// Nested capsule-name-targeted options: keys without '#' prefix
|
|
136
|
+
// are matched against capsule names deeper in the mapping tree.
|
|
137
|
+
'connectionPool': {
|
|
138
|
+
'#': { maxConnections: 10 }
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
|
|
144
|
+
// Mapping with depends: declares sibling dependencies that must resolve first.
|
|
145
|
+
// options({ self }) receives the parent capsule's self with resolved siblings.
|
|
146
|
+
// The static analyzer can auto-detect self.<name> references and inject depends.
|
|
147
|
+
$api: {
|
|
148
|
+
type: CapsulePropertyTypes.Mapping,
|
|
149
|
+
value: apiCapsule,
|
|
150
|
+
depends: ['$auth'], // explicit depends — ensures $auth is resolved first
|
|
151
|
+
options: function ({ self }: { self: any }) {
|
|
152
|
+
return {
|
|
153
|
+
'#': {
|
|
154
|
+
authRealm: self.$auth.realm,
|
|
155
|
+
capsuleName: self['#@stream44.studio/encapsulate/structs/Capsule'].capsuleName
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
|
|
161
|
+
// --- Lifecycle Properties ---
|
|
162
|
+
|
|
163
|
+
// StructInit: runs once after instantiation, before the handler. For struct capsules.
|
|
164
|
+
// Fires top-down through the extends chain. Not exposed on the API.
|
|
165
|
+
Init: {
|
|
166
|
+
type: CapsulePropertyTypes.StructInit,
|
|
167
|
+
value: async function (this: any) {
|
|
168
|
+
this.ready = true
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
// Dispose: runs after the handler completes for. For struct capsules. Reverse order (bottom-up). Not on API.
|
|
173
|
+
Dispose: {
|
|
174
|
+
type: CapsulePropertyTypes.StructDispose,
|
|
175
|
+
value: async function (this: any) {
|
|
176
|
+
this.ready = true
|
|
177
|
+
}
|
|
178
|
+
},
|
|
22
179
|
|
|
180
|
+
// Init: like StructInit but for non-struct capsules (those without StructInit).
|
|
181
|
+
Init: {
|
|
182
|
+
type: CapsulePropertyTypes.Init,
|
|
183
|
+
value: false
|
|
184
|
+
},
|
|
185
|
+
|
|
186
|
+
// Dispose: runs after the handler completes (those without StructInit). Reverse order (bottom-up). Not on API.
|
|
187
|
+
Dispose: {
|
|
188
|
+
type: CapsulePropertyTypes.Dispose,
|
|
189
|
+
value: async function (this: any) {
|
|
190
|
+
this.ready = false
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
|
|
194
|
+
// --- this.self ---
|
|
195
|
+
// Inside any function, 'this' resolves the full merged context (child + parent).
|
|
196
|
+
// 'this.self' resolves only the current capsule's own properties (ownSelf).
|
|
197
|
+
// Useful in parent capsules to distinguish own values from child overrides.
|
|
198
|
+
getOwnName: {
|
|
199
|
+
type: CapsulePropertyTypes.GetterFunction,
|
|
200
|
+
value: function (this: any): string {
|
|
201
|
+
return this.self.name // own capsule's name, not child's override
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}, {
|
|
207
|
+
importMeta: import.meta,
|
|
208
|
+
importStack: makeImportStack(),
|
|
209
|
+
capsuleName: 'UserService', // optional name — enables override targeting by name
|
|
210
|
+
extendsCapsule: baseCapsule, // inherits properties from another capsule (reference or string URI)
|
|
211
|
+
ambientReferences: { authCapsule, apiCapsule } // capsules referenced in the definition that need CST tracking
|
|
212
|
+
})
|
|
213
|
+
```
|
|
23
214
|
|
|
24
215
|
|
|
25
216
|
Reference
|
|
26
217
|
---
|
|
27
218
|
|
|
219
|
+
### Capsule Definition Structure
|
|
220
|
+
|
|
221
|
+
```
|
|
222
|
+
{
|
|
223
|
+
'<spineContractUri>': {
|
|
224
|
+
'<propertyContractUri>': { ...properties } | { as?, options? },
|
|
225
|
+
'#': { ...properties }
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
- **Spine contract URI** — prefixed with `#`. Identifies which spine contract governs property mapping. e.g. `'#@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0'`
|
|
231
|
+
- **Property contract URI** — keys starting with `#` within a spine contract. `'#'` is the default contract whose properties are exposed directly on the API. Non-default URIs (e.g. `'#./MyStruct.v0'`) are resolved as capsule mappings and mounted as sub-components.
|
|
232
|
+
- **Property definition** — `{ type, value, ...options }` where `type` is a `CapsulePropertyTypes` member.
|
|
233
|
+
|
|
234
|
+
### Encapsulate Options
|
|
235
|
+
|
|
236
|
+
| Option | Type | Description |
|
|
237
|
+
|---|---|---|
|
|
238
|
+
| `importMeta` | `{ url: string }` | `import.meta` of the defining module. Used to derive `moduleFilepath`. |
|
|
239
|
+
| `importStack` | `string` | Stack trace from `makeImportStack()`. Used to derive `importStackLine`. |
|
|
240
|
+
| `importStackLine` | `number` | Explicit line number (alternative to `importStack`). |
|
|
241
|
+
| `moduleFilepath` | `string` | Explicit module path (alternative to `importMeta`). |
|
|
242
|
+
| `capsuleName` | `string` | Optional name. Enables targeting via overrides/options by name instead of `capsuleSourceLineRef`. |
|
|
243
|
+
| `extendsCapsule` | `TCapsule \| string` | Parent capsule to inherit from. String URIs are resolved relative to the module. |
|
|
244
|
+
| `ambientReferences` | `Record<string, any>` | Named capsule references used in the definition. Required for static analysis to track cross-capsule dependencies. |
|
|
245
|
+
| `cst` / `crt` | `any` | Pre-computed CST/CRT. Bypasses static analysis. |
|
|
246
|
+
|
|
247
|
+
### CapsulePropertyTypes
|
|
248
|
+
|
|
249
|
+
#### Value Types
|
|
250
|
+
|
|
251
|
+
| Type | API Access | `this` Access | Overridable | Description |
|
|
252
|
+
|---|---|---|---|---|
|
|
253
|
+
| `Literal` | read/write | read/write | yes | General-purpose value. Supports any JS type including `Map`, `Set`, etc. |
|
|
254
|
+
| `String` | read/write | read/write | yes | Alias for `Literal`. Semantic hint for string values. |
|
|
255
|
+
| `Constant` | read-only | read | no | Immutable value. Membrane contract throws on assignment. |
|
|
256
|
+
|
|
257
|
+
All value types accept a `value` in their definition. `undefined` means "no default — must be supplied via overrides/options".
|
|
258
|
+
|
|
259
|
+
#### Function Types
|
|
260
|
+
|
|
261
|
+
| Type | API Access | Signature | Description |
|
|
262
|
+
|---|---|---|---|
|
|
263
|
+
| `Function` | `api.name(...args)` | `function(this, ...args)` | Callable method. Bound to self proxy. |
|
|
264
|
+
| `GetterFunction` | `api.name` (no parens) | `function(this)` | Lazy getter. Evaluated on each access (unless memoized). |
|
|
265
|
+
| `SetterFunction` | `api.name = value` | `function(this, value)` | Triggered on assignment. Enables validation/transformation logic. |
|
|
266
|
+
|
|
267
|
+
All function types are bound to a **self proxy** where:
|
|
268
|
+
- `this.<prop>` resolves through: self → encapsulatedApi → extendedCapsuleApi
|
|
269
|
+
- `this.self.<prop>` resolves only the current capsule's own properties (`ownSelf`)
|
|
270
|
+
|
|
271
|
+
#### Lifecycle Types
|
|
272
|
+
|
|
273
|
+
| Type | When | Order | On API | Description |
|
|
274
|
+
|---|---|---|---|---|
|
|
275
|
+
| `StructInit` | Before handler | Top-down (child → extended parent) | No | Initialization for struct capsules. Async supported. |
|
|
276
|
+
| `StructDispose` | After handler | Bottom-up (reverse of init) | No | Cleanup for struct capsules. Async supported. |
|
|
277
|
+
| `Init` | Before handler | Top-down | No | Initialization for non-struct capsules (those without `StructInit`). |
|
|
278
|
+
| `Dispose` | After handler | Bottom-up | No | Cleanup for non-struct capsules. |
|
|
279
|
+
|
|
280
|
+
Multiple lifecycle functions per capsule are supported. They execute in definition order.
|
|
281
|
+
|
|
282
|
+
#### Memoize Option
|
|
283
|
+
|
|
284
|
+
Applies to `Function` and `GetterFunction`. Added as a sibling to `type` and `value`:
|
|
285
|
+
|
|
286
|
+
```ts
|
|
287
|
+
{ type: CapsulePropertyTypes.GetterFunction, value: fn, memoize: true } // permanent cache
|
|
288
|
+
{ type: CapsulePropertyTypes.Function, value: fn, memoize: 5000 } // TTL in ms
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
Memoize caches are scoped per spine contract capsule instance and cleared automatically when `run()` completes.
|
|
292
|
+
|
|
293
|
+
### Mapping
|
|
294
|
+
|
|
295
|
+
`Mapping` composes another capsule as a sub-component.
|
|
296
|
+
|
|
297
|
+
```ts
|
|
298
|
+
prop: {
|
|
299
|
+
type: CapsulePropertyTypes.Mapping,
|
|
300
|
+
value: capsuleRef | './relative/path',
|
|
301
|
+
options: { '#': { key: value } } // static object
|
|
302
|
+
options: async ({ self, constants }) => ({ '#': { ... } }) // dynamic function
|
|
303
|
+
depends: ['siblingPropName'] // optional
|
|
304
|
+
}
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
- **`value`** — a capsule reference (from `encapsulate()`) or a string URI resolved relative to the current module.
|
|
308
|
+
- **`options`** — forwarded to the mapped capsule. Keys starting with `'#'` target the mapped capsule's own property contracts. Keys without `'#'` are matched against capsule names deeper in the mapping tree (nested capsule-name-targeted options).
|
|
309
|
+
- **`options({ self, constants })`** — when `options` is a function, it receives `{ self, constants }`.
|
|
310
|
+
- `constants` — all `Literal`/`String` values from the mapped capsule's definition.
|
|
311
|
+
- `self` — the parent capsule's `self` object with resolved sibling mappings. Only populated when `depends` is specified (empty `{}` otherwise).
|
|
312
|
+
- **`depends`** — array of sibling property names that must be resolved before this mapping's `options` function runs. Enables `options({ self })` to access already-resolved siblings (e.g. `self.$auth.realm`) and the Capsule metadata struct (e.g. `self['#@stream44.studio/encapsulate/structs/Capsule'].capsuleName`). Can be declared explicitly or auto-injected by the static analyzer when it detects `self.<name>` references in the options function body.
|
|
313
|
+
- **Instance reuse** — named capsules are registered in an instance registry. If a capsule with the same name is mapped multiple times without options, the existing instance is reused via a deferred proxy.
|
|
314
|
+
|
|
315
|
+
Mapped capsules are accessible via `this.<prop>` (unwrapped API) and `api.<prop>` (raw instance with `.api`).
|
|
316
|
+
|
|
317
|
+
### importCapsule
|
|
318
|
+
|
|
319
|
+
`this.self.importCapsule()` dynamically loads and initializes a capsule by URI at runtime — without pre-declaring it as a `Mapping` property. The imported capsule is **not** mapped onto the parent capsule's API.
|
|
320
|
+
|
|
321
|
+
```ts
|
|
322
|
+
run: {
|
|
323
|
+
type: CapsulePropertyTypes.Function,
|
|
324
|
+
value: async function (this: any) {
|
|
325
|
+
const { capsule, api } = await this.self.importCapsule({
|
|
326
|
+
uri: '@scope/package/caps/MyCapsule', // or './relative/path'
|
|
327
|
+
options: { '#': { key: 'value' } }, // optional — forwarded to makeInstance
|
|
328
|
+
overrides: { ... } // optional — forwarded to makeInstance
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
// Use the imported capsule's API directly
|
|
332
|
+
await api.doSomething()
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
- **`uri`** — a string URI resolved using the same mechanism as `Mapping` values (relative paths, `@scope/package/path`, etc.).
|
|
338
|
+
- **`options`** — forwarded to the imported capsule's `makeInstance()`. Same structure as mapping options.
|
|
339
|
+
- **`overrides`** — forwarded to the imported capsule's `makeInstance()`. Same structure as runtime overrides.
|
|
340
|
+
- **Returns** `{ capsule, api }` — the resolved capsule object and its initialized API.
|
|
341
|
+
- The imported capsule receives the caller's runtime spine contracts and root capsule context.
|
|
342
|
+
- Init lifecycle functions are executed on the imported capsule instance before returning.
|
|
343
|
+
- The imported capsule is **not** registered in the instance registry and **not** mounted on the parent's API or `self`.
|
|
344
|
+
|
|
345
|
+
Use `importCapsule` when you need to work with arbitrary capsules determined at runtime without declaring them all upfront as `Mapping` properties.
|
|
346
|
+
|
|
347
|
+
### Property Contract Delegates
|
|
348
|
+
|
|
349
|
+
Non-default property contract URIs (e.g. `'#./MyStruct.v0'`) are resolved as capsule mappings and automatically mounted.
|
|
350
|
+
|
|
351
|
+
```ts
|
|
352
|
+
'#./MyStruct.v0': {
|
|
353
|
+
as: '$myStruct', // alias — accessible as api.$myStruct and this.$myStruct
|
|
354
|
+
options: { '#': { key: value } } // forwarded to the struct capsule
|
|
355
|
+
}
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
Without `as`, the property is accessible via `api['#./MyStruct.v0']`. The delegate's properties are also mounted under `api['#./MyStruct.v0']` as a namespace.
|
|
359
|
+
|
|
360
|
+
Overrides targeting a property contract delegate use the delegate URI as key:
|
|
361
|
+
|
|
362
|
+
```ts
|
|
363
|
+
overrides: {
|
|
364
|
+
'capsuleName': {
|
|
365
|
+
'#./MyStruct.v0': { key: 'overridden' }
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
### Extends
|
|
371
|
+
|
|
372
|
+
A capsule can inherit properties from a parent capsule:
|
|
373
|
+
|
|
374
|
+
```ts
|
|
375
|
+
{ extendsCapsule: parentCapsule } // direct reference
|
|
376
|
+
{ extendsCapsule: './BaseCapsule.v0' } // string URI, resolved relative to module
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
- Child and parent share the same `self` object. Parent functions see child's property values.
|
|
380
|
+
- Child properties take precedence over parent properties with the same name.
|
|
381
|
+
- The API uses a proxy: local properties are checked first, then the extended capsule's API.
|
|
382
|
+
- `this.self` in a parent function returns the parent's own values (`ownSelf`), not the merged context.
|
|
383
|
+
- Multiple capsules can extend the same parent — each gets a separate parent instance with its own `self`.
|
|
384
|
+
|
|
385
|
+
### Spine Contracts: Static.v0 vs Membrane.v0
|
|
386
|
+
|
|
387
|
+
Both implement the same property mapping logic. The difference is observability.
|
|
388
|
+
|
|
389
|
+
**Static.v0** — direct property assignment. No interception. Minimal overhead.
|
|
390
|
+
|
|
391
|
+
**Membrane.v0** — wraps the API in proxies that emit events for every property access:
|
|
392
|
+
|
|
393
|
+
| Event | Emitted When | Payload |
|
|
394
|
+
|---|---|---|
|
|
395
|
+
| `get` | Property read | `{ target, value, eventIndex }` |
|
|
396
|
+
| `set` | Property write | `{ target, value, eventIndex }` |
|
|
397
|
+
| `call` | Function invoked | `{ target, args, eventIndex }` |
|
|
398
|
+
| `call-result` | Function returns | `{ target, result, callEventIndex }` |
|
|
399
|
+
|
|
400
|
+
Events include `caller` context (source capsule, property, filepath, line) when `enableCallerStackInference` is enabled. Memoized results are tagged with `memoized: true`.
|
|
401
|
+
|
|
402
|
+
### SpineRuntime & run()
|
|
403
|
+
|
|
404
|
+
```ts
|
|
405
|
+
const { run } = await SpineRuntime({ spineContracts, capsules, snapshot? })
|
|
406
|
+
|
|
407
|
+
const result = await run({
|
|
408
|
+
overrides: {
|
|
409
|
+
'capsuleName': { '#': { prop: 'value' } } // by name
|
|
410
|
+
'path/to/file.ts:42': { '#': { prop: 'value' } } // by capsuleSourceLineRef
|
|
411
|
+
},
|
|
412
|
+
options: {
|
|
413
|
+
'capsuleName': { '#': { prop: 'value' } }
|
|
414
|
+
}
|
|
415
|
+
}, async ({ apis }) => {
|
|
416
|
+
return apis['capsuleName'].greet('Hello')
|
|
417
|
+
})
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
- **`overrides`** — merged into `self` before instantiation. Applied by `capsuleSourceLineRef` first, then by `capsuleName`.
|
|
421
|
+
- **`options`** — passed to `makeInstance()`. Same structure as overrides.
|
|
422
|
+
- **`apis`** — proxy-wrapped capsule instances. Nested `.api` layers are automatically unwrapped.
|
|
423
|
+
|
|
424
|
+
Lifecycle: instantiate → StructInit/Init → handler → StructDispose/Dispose → clear memoize timeouts.
|
|
425
|
+
|
|
426
|
+
### Capsule Module Format
|
|
427
|
+
|
|
428
|
+
Capsule source files export a `capsule` function:
|
|
429
|
+
|
|
430
|
+
```ts
|
|
431
|
+
export async function capsule({ encapsulate, CapsulePropertyTypes, makeImportStack }) {
|
|
432
|
+
return await encapsulate({ ... }, {
|
|
433
|
+
importMeta: import.meta,
|
|
434
|
+
importStack: makeImportStack(),
|
|
435
|
+
capsuleName: capsule['#']
|
|
436
|
+
})
|
|
437
|
+
}
|
|
438
|
+
capsule['#'] = '@my-org/my-package/MyCapsule'
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
The `capsule['#']` convention provides a stable identifier for the capsule used in resolution and registry.
|
|
442
|
+
|
|
443
|
+
### Capsule Metadata Struct
|
|
444
|
+
|
|
445
|
+
Every capsule with `'#@stream44.studio/encapsulate/structs/Capsule': {}` gets metadata injected into `self` and exposed on the API:
|
|
446
|
+
|
|
447
|
+
```ts
|
|
448
|
+
api['#@stream44.studio/encapsulate/structs/Capsule'] = {
|
|
449
|
+
capsuleName,
|
|
450
|
+
capsuleSourceLineRef, // absolute path:line
|
|
451
|
+
moduleFilepath, // absolute path
|
|
452
|
+
rootCapsule: { // the top-level capsule in the extends chain
|
|
453
|
+
capsuleName,
|
|
454
|
+
capsuleSourceLineRef,
|
|
455
|
+
moduleFilepath
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
`capsuleSourceNameRefHash` is also available when static analysis is enabled.
|
|
461
|
+
|
|
462
|
+
---
|
|
28
463
|
|
|
464
|
+
(c) 2026 [Christoph.diy](https://christoph.diy) • Code: [MIT](../../../LICENSE.txt) • Text: [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) • Created with [Stream44.Studio](https://Stream44.Studio)
|
|
@@ -40,6 +40,42 @@ export class ContractCapsuleInstanceFactory {
|
|
|
40
40
|
this.extendedCapsuleInstance = extendedCapsuleInstance
|
|
41
41
|
this.runtimeSpineContracts = runtimeSpineContracts
|
|
42
42
|
this.capsuleInstance = capsuleInstance
|
|
43
|
+
|
|
44
|
+
// Inject importCapsule onto ownSelf so capsule functions can call this.self.importCapsule()
|
|
45
|
+
if (ownSelf && !ownSelf.importCapsule) {
|
|
46
|
+
ownSelf.importCapsule = this.handleImportCapsule.bind(this)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async handleImportCapsule({ uri, options, overrides }: { uri: string, options?: Record<string, any>, overrides?: Record<string, any> }): Promise<{ capsule: any, api: any }> {
|
|
51
|
+
// Resolve the URI to a capsule object using the same mechanism as mappings
|
|
52
|
+
const resolvedCapsule = await this.resolveMappedCapsule({
|
|
53
|
+
property: {
|
|
54
|
+
name: '__importCapsule__',
|
|
55
|
+
definition: { value: uri }
|
|
56
|
+
}
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
// Instantiate the capsule with the caller's runtime spine contracts and optional overrides/options
|
|
60
|
+
const instance = await resolvedCapsule.makeInstance({
|
|
61
|
+
overrides: overrides || {},
|
|
62
|
+
options: options,
|
|
63
|
+
runtimeSpineContracts: this.runtimeSpineContracts,
|
|
64
|
+
rootCapsule: this.capsuleInstance?.rootCapsule
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
// Run init functions on the imported capsule instance
|
|
68
|
+
if (instance.initFunctions?.length) {
|
|
69
|
+
for (const fn of instance.initFunctions) {
|
|
70
|
+
await fn()
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Return capsule and unwrapped api without mapping onto the parent
|
|
75
|
+
return {
|
|
76
|
+
capsule: resolvedCapsule,
|
|
77
|
+
api: instance.api || instance
|
|
78
|
+
}
|
|
43
79
|
}
|
|
44
80
|
|
|
45
81
|
async mapProperty({ overrides, options, property }: { overrides: any, options: any, property: any }) {
|
|
@@ -158,10 +194,12 @@ export class ContractCapsuleInstanceFactory {
|
|
|
158
194
|
|
|
159
195
|
// delegateOptions is set by encapsulate.ts for property contract delegates
|
|
160
196
|
// options can be a function or an object for regular mappings
|
|
197
|
+
// Always pass { self, constants } - self is populated when depends is specified, empty otherwise
|
|
198
|
+
const optionsFn = property.definition.options
|
|
161
199
|
const mappingOptions = property.definition.delegateOptions
|
|
162
|
-
|| (typeof
|
|
163
|
-
? await property.definition.
|
|
164
|
-
:
|
|
200
|
+
|| (typeof optionsFn === 'function'
|
|
201
|
+
? await optionsFn({ self: property.definition.depends ? this.self : {}, constants })
|
|
202
|
+
: optionsFn)
|
|
165
203
|
|
|
166
204
|
// Check for existing instance in registry - reuse if available when no options
|
|
167
205
|
// Pre-registration with null allows parent capsules to "claim" a slot before child capsules process
|
|
@@ -486,13 +486,31 @@ export async function CapsuleSpineFactory({
|
|
|
486
486
|
timing,
|
|
487
487
|
cacheStore: {
|
|
488
488
|
writeFile: async (filepath: string, content: string) => {
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
await
|
|
489
|
+
// Write to central cache (spineFilesystemRoot)
|
|
490
|
+
const centralPath = join(spineFilesystemRoot, '.~o/encapsulate.dev/static-analysis', filepath)
|
|
491
|
+
await mkdir(dirname(centralPath), { recursive: true })
|
|
492
|
+
await writeFile(centralPath, content, 'utf-8')
|
|
493
|
+
// Also write to local project cache if available
|
|
494
|
+
if (capsuleModuleProjectionRoot) {
|
|
495
|
+
try {
|
|
496
|
+
const localPath = join(capsuleModuleProjectionRoot, '.~o/encapsulate.dev/static-analysis', filepath)
|
|
497
|
+
await mkdir(dirname(localPath), { recursive: true })
|
|
498
|
+
await writeFile(localPath, content, 'utf-8')
|
|
499
|
+
} catch { /* local write is best-effort */ }
|
|
500
|
+
}
|
|
492
501
|
},
|
|
493
502
|
readFile: async (filepath: string) => {
|
|
494
|
-
|
|
495
|
-
|
|
503
|
+
const centralPath = join(spineFilesystemRoot, '.~o/encapsulate.dev/static-analysis', filepath)
|
|
504
|
+
const content = await readFile(centralPath, 'utf-8')
|
|
505
|
+
// Sync to local project cache on read (covers cache-hit paths)
|
|
506
|
+
if (content && capsuleModuleProjectionRoot) {
|
|
507
|
+
try {
|
|
508
|
+
const localPath = join(capsuleModuleProjectionRoot, '.~o/encapsulate.dev/static-analysis', filepath)
|
|
509
|
+
await mkdir(dirname(localPath), { recursive: true })
|
|
510
|
+
await writeFile(localPath, content, 'utf-8')
|
|
511
|
+
} catch { /* local sync is best-effort */ }
|
|
512
|
+
}
|
|
513
|
+
return content
|
|
496
514
|
},
|
|
497
515
|
getStats: async (filepath: string) => {
|
|
498
516
|
filepath = join(spineFilesystemRoot, '.~o/encapsulate.dev/static-analysis', filepath)
|