@@ -173,18 +173,33 @@ export type { MemorySpace } from "@commontools/memory/interface";
173173
174174export function createCell < T > (
175175 runtime : IRuntime ,
176- link : NormalizedFullLink | undefined ,
176+ link ? : NormalizedFullLink ,
177177 tx ?: IExtendedStorageTransaction ,
178178 synced = false ,
179179) : Cell < T > {
180180 return new CellImpl (
181181 runtime ,
182- link ,
183182 tx ,
183+ link , // Pass the link directly (or undefined)
184184 synced ,
185+ undefined , // No shared causeContainer
185186 ) as unknown as Cell < T > ; // Cast to set brand
186187}
187188
189+ /**
190+ * Shared container for entity ID and cause information across sibling cells.
191+ * When cells are created via .asSchema(), .withTx(), they share the same
192+ * logical identity (same entity id) but may have different paths or schemas.
193+ * The container stores only the entity reference parts that need to be synchronized.
194+ */
195+ interface CauseContainer {
196+ // Entity reference - shared across all siblings
197+ id : URI | undefined ;
198+ space : MemorySpace | undefined ;
199+ // Cause for creating the entity ID
200+ cause : unknown | undefined ;
201+ }
202+
188203/**
189204 * CellImpl - Unified cell implementation that handles both regular cells and
190205 * streams.
@@ -198,39 +213,50 @@ export class CellImpl<T> implements ICell<T>, IStreamable<T> {
198213 > ( ) ;
199214 private cleanup : Cancel | undefined ;
200215
201- // Use NormalizedLink which may not have id/ space yet
216+ // Each cell has its own link ( space, path, schema)
202217 private _link : NormalizedLink ;
203- private _cause : unknown | undefined ;
218+
219+ // Shared container for entity ID and cause - siblings share the same instance
220+ private _causeContainer : CauseContainer ;
204221
205222 constructor (
206223 public readonly runtime : IRuntime ,
207- link : NormalizedLink = { path : [ ] } ,
208224 public readonly tx : IExtendedStorageTransaction | undefined ,
225+ link ?: NormalizedLink ,
209226 private synced : boolean = false ,
210- cause ?: unknown ,
227+ causeContainer ?: CauseContainer ,
211228 ) {
212- // Always have at least a path
213- this . _link = link ;
214- this . _cause = cause ;
229+ // Store this cell's own link
230+ this . _link = link ?? { path : [ ] } ;
231+
232+ // Use provided container or create one
233+ // If link has an id, extract it to the container
234+ this . _causeContainer = causeContainer ?? {
235+ id : this . _link . id ,
236+ space : this . _link . space ,
237+ cause : undefined ,
238+ } ;
215239 }
216240
217241 /**
218242 * Get the full link for this cell, ensuring it has id and space.
219243 * This will attempt to create a full link if one doesn't exist and we're in a valid context.
220244 */
221245 private get link ( ) : NormalizedFullLink {
222- // Check if we have a full link (with id and space)
223- if ( ! this . _link . id || ! this . _link . space ) {
246+ // Check if we have a full entity ID and space
247+ if ( ! this . hasFullLink ( ) ) {
224248 // Try to ensure we have a full link
225249 this . ensureLink ( ) ;
226250
227251 // If still no full link after ensureLink, throw
228- if ( ! this . _link . id || ! this . _link . space ) {
252+ if ( ! this . hasFullLink ( ) ) {
229253 throw new Error (
230254 "Cell link could not be created. Use .for() to set a cause before accessing the cell." ,
231255 ) ;
232256 }
233257 }
258+
259+ // Combine causeContainer id with link's space/path/schema
234260 return this . _link as NormalizedFullLink ;
235261 }
236262
@@ -243,115 +269,138 @@ export class CellImpl<T> implements ICell<T>, IStreamable<T> {
243269
244270 /**
245271 * Set a cause for this cell. This is used to create a link when the cell doesn't have one yet.
272+ * This affects all sibling cells (created via .key(), .asSchema(), .withTx()) since they
273+ * share the same container.
246274 * @param cause - The cause to associate with this cell
247275 * @param options - Optional configuration
248- * @param options.force - If true, will create an extension if cause already exists. If false (default), ignores the call if link already exists.
276+ * @param options.force - If true, will create an extension if link already exists. If false (default), ignores the call if link already exists.
249277 * @returns This cell for method chaining
250278 */
251279 for ( cause : unknown , options ?: { force ?: boolean } ) : Cell < T > {
252280 const force = options ?. force ?? false ;
253281
254- // If full link already exists and force is false, ignore this call
255- if ( this . hasFullLink ( ) && ! force ) {
282+ // If cause or id already exists and force is false, silently ignore
283+ if ( ( this . _causeContainer . id || this . _causeContainer . cause ) && ! force ) {
256284 return this as unknown as Cell < T > ;
257285 }
258286
259- // Store the cause
260- this . _cause = cause ;
287+ // Store the cause in the shared container - all siblings will see this
288+ this . _causeContainer . cause = cause ;
261289
262- // TODO(seefeld): Implement link creation from cause
263- // For now, we'll defer link creation until it's actually needed
264- // This will be implemented in the "force creation of cause" step
290+ // TODO(seefeld): Implement extension creation when force is true and link exists
265291
266292 return this as unknown as Cell < T > ;
267293 }
268294
269295 /**
270296 * Force creation of a full link for this cell from the stored cause.
271- * This method populates id and space if they don 't exist, using information from:
297+ * This method populates id if it doesn 't exist, using information from:
272298 * - The stored cause (from .for())
273299 * - The current handler context
274300 * - Derived information from the graph (for deriving nodes)
275301 *
302+ * Updates the shared causeContainer, so all siblings will see the new id.
303+ *
276304 * @throws Error if not in a handler context and no cause was provided
277305 */
278306 private ensureLink ( ) : void {
279307 // If we already have a full link (id and space), nothing to do
280- if ( this . _link . id && this . _link . space ) {
308+ if ( this . _causeContainer . id && this . _link . space ) {
281309 return ;
282310 }
283311
284312 // Check if we're in a handler context
285313 const frame = getTopFrame ( ) ;
286314
287- // TODO(seefeld): Implement no-cause-but-in-handler case
288- if ( ! frame ?. cause || ! this . _cause ) {
315+ if ( ! frame ) {
289316 throw new Error (
290- "Cannot create cell link: not in a handler context and no cause was provided .\n" +
317+ "Cannot create cell link: no frame context .\n" +
291318 "This typically happens when:\n" +
292319 " - A cell is passed to another cell's .set() method without a link\n" +
293- " - A cell is used outside of a handler context\n" +
294- "Solution: Use .for(cause) to set a cause before using the cell in ambiguous cases." ,
320+ " - A cell is used outside of a handler or lift context\n" ,
295321 ) ;
296322 }
297323
324+ const space = this . _link . space ?? this . _causeContainer . space ??
325+ frame ?. space ;
326+
298327 // We need a space to create a link
299- // TODO(seefeld): Get space from frame
300- if ( ! this . _link . space ) {
328+ if ( ! space ) {
301329 throw new Error (
302330 "Cannot create cell link: space is required.\n" +
303- "When creating cells without links, you must provide a space in the link.\n" +
304- "Use runtime.getCell() or provide a link with a space when constructing the cell." ,
331+ "When creating cells without links, you must provide a space in the frame.\n" ,
332+ ) ;
333+ }
334+
335+ const cause = this . _causeContainer . cause ??
336+ ( frame . event ? { count : frame . generatedIdCounter ++ } : undefined ) ;
337+ // TODO(seefeld): Implement no-cause-but-in-handler case
338+ if ( ! cause ) {
339+ throw new Error (
340+ "Cannot create cell link: not in a handler context and no cause was provided.\n" +
341+ "This typically happens when:\n" +
342+ " - A cell is passed to another cell's .set() method without a link\n" +
343+ " - A cell is used outside of a handler context\n" +
344+ "Solution: Use .for(cause) to set a cause before using the cell in ambiguous cases." ,
305345 ) ;
306346 }
307347
308348 // Create an entity ID from the cause
309- const entityId = createRef ( { frame : frame ! . cause } , this . _cause ) ;
349+ // Include frame.cause in the source for determinism
350+ const id = toURI ( createRef ( { frame : cause } , cause ) ) ;
310351
311- // Populate the id and type fields (keeping existing path, schema, etc.)
312- this . _link = {
313- ...this . _link ,
314- id : toURI ( entityId ) ,
315- type : this . _link . type ?? "application/json" ,
316- } ;
352+ // Populate the id in the shared causeContainer
353+ // All siblings will see this update
354+ this . _causeContainer . id = id ;
355+ this . _causeContainer . space = space ;
356+
357+ // Update this cell's link with the space if it doesn't have one
358+ if ( ! this . _link . space ) {
359+ this . _link = { ...this . _link , id, space } ;
360+ }
317361 }
318362
319363 get space ( ) : MemorySpace {
320- return this . link . space ;
364+ return this . _link . space ?? this . _causeContainer . space ??
365+ getTopFrame ( ) ?. space ! ;
321366 }
322367
323368 get path ( ) : readonly PropertyKey [ ] {
324369 return this . link . path ;
325370 }
326371
327372 get schema ( ) : JSONSchema | undefined {
328- if ( ! this . _link ) return undefined ;
329-
330- if ( this . link . schema ) return this . link . schema ;
373+ if ( this . _link . schema ) return this . _link . schema ;
331374
332375 // If no schema is defined, resolve link and get schema from there (which is
333376 // what .get() would do).
334- const resolvedLink = resolveLink (
335- this . runtime . readTx ( this . tx ) ,
336- this . link ,
337- "writeRedirect" ,
338- ) ;
339- return resolvedLink . schema ;
377+ if ( this . hasFullLink ( ) ) {
378+ const resolvedLink = resolveLink (
379+ this . runtime . readTx ( this . tx ) ,
380+ this . link ,
381+ "writeRedirect" ,
382+ ) ;
383+ return resolvedLink . schema ;
384+ }
385+
386+ return undefined ;
340387 }
341388
342389 get rootSchema ( ) : JSONSchema | undefined {
343- if ( ! this . _link ) return undefined ;
344-
345- if ( this . link . rootSchema ) return this . link . rootSchema ;
390+ if ( this . _link . rootSchema ) return this . _link . rootSchema ;
346391
347392 // If no root schema is defined, resolve link and get root schema from there
348393 // (which is what .get() would do).
349- const resolvedLink = resolveLink (
350- this . runtime . readTx ( this . tx ) ,
351- this . link ,
352- "writeRedirect" ,
353- ) ;
354- return resolvedLink . rootSchema ;
394+ if ( this . hasFullLink ( ) ) {
395+ const resolvedLink = resolveLink (
396+ this . runtime . readTx ( this . tx ) ,
397+ this . link ,
398+ "writeRedirect" ,
399+ ) ;
400+ return resolvedLink . rootSchema ;
401+ }
402+
403+ return undefined ;
355404 }
356405
357406 /**
@@ -554,19 +603,20 @@ export class CellImpl<T> implements ICell<T>, IStreamable<T> {
554603 )
555604 : undefined ;
556605
557- // Build up the path even without a full link
606+ // Create a child link with extended path
558607 const childLink : NormalizedLink = {
559608 ...this . _link ,
560609 path : [ ...this . _link . path , valueKey . toString ( ) ] as string [ ] ,
561- ...( childSchema && { schema : childSchema } ) ,
610+ schema : childSchema ,
611+ rootSchema : childSchema ? this . _link . rootSchema : undefined ,
562612 } ;
563613
564614 return new CellImpl (
565615 this . runtime ,
566- childLink ,
567616 this . tx ,
617+ childLink ,
568618 this . synced ,
569- this . _cause , // Inherit cause
619+ this . _causeContainer ,
570620 ) as unknown as KeyResultType < T , K , AsCell > ;
571621 }
572622
@@ -577,21 +627,32 @@ export class CellImpl<T> implements ICell<T>, IStreamable<T> {
577627 schema ?: JSONSchema ,
578628 ) : Cell < T > ;
579629 asSchema ( schema ?: JSONSchema ) : Cell < any > {
630+ // asSchema creates a sibling with same identity but different schema
631+ // Create a new link with modified schema
632+ const siblingLink : NormalizedLink = {
633+ ...this . _link ,
634+ schema : schema ,
635+ rootSchema : schema ,
636+ } ;
637+
580638 return new CellImpl (
581639 this . runtime ,
582- { ...this . link , schema : schema , rootSchema : schema } ,
583640 this . tx ,
641+ siblingLink ,
584642 false , // Reset synced flag, since schema is changing
643+ this . _causeContainer , // Share the causeContainer with siblings
585644 ) as unknown as Cell < any > ;
586645 }
587646
588647 withTx ( newTx ?: IExtendedStorageTransaction ) : Cell < T > {
589- // For streams, this is a no-op, but we still create a new instance
648+ // withTx creates a sibling with same identity but different transaction
649+ // Share the causeContainer so .for() calls propagate
590650 return new CellImpl (
591651 this . runtime ,
592- this . link ,
593652 newTx ,
653+ this . _link , // Use the same link
594654 this . synced ,
655+ this . _causeContainer , // Share the causeContainer with siblings
595656 ) as unknown as Cell < T > ;
596657 }
597658
0 commit comments