@@ -18,6 +18,7 @@ import {
1818 UI ,
1919 URI ,
2020} from "@commontools/runner" ;
21+ import { ALL_CHARMS_ID } from "../../runner/src/builtins/well-known.ts" ;
2122import { vdomSchema } from "@commontools/html" ;
2223import { type Session } from "@commontools/identity" ;
2324import { isObject , isRecord } from "@commontools/utils/types" ;
@@ -55,6 +56,18 @@ export const charmListSchema = {
5556 items : { asCell : true } ,
5657} as const satisfies JSONSchema ;
5758
59+ export const spaceCellSchema = {
60+ type : "object" ,
61+ properties : {
62+ // Each field is a Cell<> reference to another cell
63+ allCharms : { ...charmListSchema , asCell : true } ,
64+ defaultPattern : { type : "object" , asCell : true } ,
65+ recentCharms : { ...charmListSchema , asCell : true } ,
66+ } ,
67+ } as const satisfies JSONSchema ;
68+
69+ export type SpaceCell = Schema < typeof spaceCellSchema > ;
70+
5871export const charmLineageSchema = {
5972 type : "object" ,
6073 properties : {
@@ -120,7 +133,8 @@ export class CharmManager {
120133
121134 private charms : Cell < Cell < unknown > [ ] > ;
122135 private pinnedCharms : Cell < Cell < unknown > [ ] > ;
123- private trashedCharms : Cell < Cell < unknown > [ ] > ;
136+ private recentCharms : Cell < Cell < unknown > [ ] > ;
137+ private spaceCell : Cell < SpaceCell > ;
124138
125139 /**
126140 * Promise resolved when the charm manager gets the charm list.
@@ -133,26 +147,77 @@ export class CharmManager {
133147 ) {
134148 this . space = this . session . space ;
135149
136- this . charms = this . runtime . getCell (
150+ // Use the well-known ALL_CHARMS_ID entity for the charms cell
151+ this . charms = this . runtime . getCellFromEntityId (
137152 this . space ,
138- "charms" ,
153+ { "/" : ALL_CHARMS_ID } ,
154+ [ ] ,
139155 charmListSchema ,
140156 ) ;
141157 this . pinnedCharms = this . runtime . getCell (
142158 this . space ,
143159 "pinned-charms" ,
144160 charmListSchema ,
145161 ) ;
146- this . trashedCharms = this . runtime . getCell (
162+ // Use the space DID as the cause - it's derived from the space name
163+ // and consistently available everywhere
164+ this . spaceCell = this . runtime . getCell (
147165 this . space ,
148- "trash" ,
149- charmListSchema ,
166+ this . space , // Space DID is stable per space and available to all clients
167+ spaceCellSchema ,
168+ ) ;
169+
170+ const syncSpaceCell = Promise . resolve ( this . spaceCell . sync ( ) ) ;
171+
172+ // Initialize the space cell structure by linking to existing cells
173+ const linkSpaceCell = syncSpaceCell . then ( ( ) =>
174+ this . runtime . editWithRetry ( ( tx ) => {
175+ const spaceCellWithTx = this . spaceCell . withTx ( tx ) ;
176+
177+ let existingSpace : Partial < SpaceCell > | undefined ;
178+ try {
179+ existingSpace = spaceCellWithTx . get ( ) ?? undefined ;
180+ } catch {
181+ existingSpace = undefined ;
182+ }
183+
184+ const recentCharmsField = spaceCellWithTx
185+ . key ( "recentCharms" )
186+ . asSchema ( charmListSchema ) ;
187+
188+ let recentCharmsValue : unknown ;
189+ try {
190+ recentCharmsValue = recentCharmsField . get ( ) ;
191+ } catch {
192+ recentCharmsValue = undefined ;
193+ }
194+
195+ if ( ! Array . isArray ( recentCharmsValue ) ) {
196+ recentCharmsField . set ( [ ] ) ;
197+ }
198+
199+ const nextSpaceValue : Partial < SpaceCell > = {
200+ ...( existingSpace ?? { } ) ,
201+ allCharms : this . charms . withTx ( tx ) ,
202+ recentCharms : recentCharmsField . withTx ( tx ) ,
203+ } ;
204+
205+ spaceCellWithTx . set ( nextSpaceValue ) ;
206+
207+ // defaultPattern will be linked later when the default pattern is found
208+ } )
150209 ) ;
151210
211+ this . recentCharms = this . spaceCell
212+ . key ( "recentCharms" )
213+ . asSchema ( charmListSchema ) ;
214+
152215 this . ready = Promise . all ( [
153216 this . syncCharms ( this . charms ) ,
154217 this . syncCharms ( this . pinnedCharms ) ,
155- this . syncCharms ( this . trashedCharms ) ,
218+ this . syncCharms ( this . recentCharms ) ,
219+ syncSpaceCell ,
220+ linkSpaceCell ,
156221 ] ) . then ( ( ) => { } ) ;
157222 }
158223
@@ -210,47 +275,51 @@ export class CharmManager {
210275 return this . pinnedCharms ;
211276 }
212277
213- getTrash ( ) : Cell < Cell < unknown > [ ] > {
214- this . syncCharms ( this . trashedCharms ) ;
215- return this . trashedCharms ;
278+ getSpaceCell ( ) : Cell < SpaceCell > {
279+ return this . spaceCell ;
216280 }
217281
218- async restoreFromTrash ( idOrCharm : string | EntityId | Cell < unknown > ) {
219- await this . syncCharms ( this . trashedCharms ) ;
220- await this . syncCharms ( this . charms ) ;
282+ /**
283+ * Link the default pattern cell to the space cell.
284+ * This should be called after the default pattern is created.
285+ * @param defaultPatternCell - The cell representing the default pattern
286+ */
287+ async linkDefaultPattern (
288+ defaultPatternCell : Cell < any > ,
289+ ) : Promise < void > {
290+ await this . runtime . editWithRetry ( ( tx ) => {
291+ const spaceCellWithTx = this . spaceCell . withTx ( tx ) ;
292+ spaceCellWithTx . key ( "defaultPattern" ) . set ( defaultPatternCell . withTx ( tx ) ) ;
293+ } ) ;
294+ await this . runtime . idle ( ) ;
295+ }
221296
222- const error = await this . runtime . editWithRetry ( ( tx ) => {
223- const trashedCharms = this . trashedCharms . withTx ( tx ) ;
297+ /**
298+ * Track a charm as recently viewed/interacted with.
299+ * Maintains a list of up to 10 most recent charms.
300+ * @param charm - The charm to track
301+ */
302+ async trackRecentCharm ( charm : Cell < unknown > ) : Promise < void > {
303+ const id = getEntityId ( charm ) ;
304+ if ( ! id ) return ;
224305
225- const id = getEntityId ( idOrCharm ) ;
226- if ( ! id ) return false ;
306+ await this . runtime . editWithRetry ( ( tx ) => {
307+ const recentCharmsWithTx = this . recentCharms . withTx ( tx ) ;
308+ const recentCharms = recentCharmsWithTx . get ( ) || [ ] ;
227309
228- // Find the charm in trash
229- const trashedCharm = trashedCharms . get ( ) . find ( ( charm ) =>
230- isSameEntity ( charm , id )
231- ) ;
310+ // Remove any existing instance of this charm to avoid duplicates
311+ const filtered = recentCharms . filter ( ( c ) => ! isSameEntity ( c , id ) ) ;
232312
233- if ( ! trashedCharm ) return false ;
313+ // Add charm to the beginning of the list
314+ const updated = [ charm , ...filtered ] ;
234315
235- // Remove from trash
236- const newTrashedCharms = filterOutEntity ( trashedCharms , id ) ;
237- trashedCharms . set ( newTrashedCharms ) ;
316+ // Trim to max 10 items
317+ const trimmed = updated . slice ( 0 , 10 ) ;
238318
239- // Add back to charms
240- this . addCharms ( [ trashedCharm ] , tx ) ;
319+ recentCharmsWithTx . set ( trimmed ) ;
241320 } ) ;
242321
243322 await this . runtime . idle ( ) ;
244-
245- return ! error ;
246- }
247-
248- async emptyTrash ( ) {
249- await this . syncCharms ( this . trashedCharms ) ;
250- await this . runtime . editWithRetry ( ( tx ) => {
251- const trashedCharms = this . trashedCharms . withTx ( tx ) ;
252- trashedCharms . set ( [ ] ) ;
253- } ) ;
254323 }
255324
256325 // FIXME(ja): this says it returns a list of charm, but it isn't! you will
@@ -821,14 +890,12 @@ export class CharmManager {
821890 }
822891
823892 // note: removing a charm doesn't clean up the charm's cells
824- // Now moves the charm to trash instead of just removing it
825893 async remove ( idOrCharm : string | EntityId | Cell < unknown > ) {
826894 let success = false ;
827895
828896 await Promise . all ( [
829897 this . syncCharms ( this . charms ) ,
830898 this . syncCharms ( this . pinnedCharms ) ,
831- this . syncCharms ( this . trashedCharms ) ,
832899 ] ) ;
833900
834901 const id = getEntityId ( idOrCharm ) ;
@@ -838,44 +905,11 @@ export class CharmManager {
838905
839906 return ( ! await this . runtime . editWithRetry ( ( tx ) => {
840907 const charms = this . charms . withTx ( tx ) ;
841- const trashedCharms = this . trashedCharms . withTx ( tx ) ;
842-
843- // Find the charm in the main list
844- const charm = charms . get ( ) . find ( ( c ) => isSameEntity ( c , id ) ) ;
845- if ( ! charm ) {
846- success = false ;
847- } else {
848- // Move to trash if not already there
849- if ( ! trashedCharms . get ( ) . some ( ( c ) => isSameEntity ( c , id ) ) ) {
850- trashedCharms . push ( charm ) ;
851- }
852-
853- // Remove from main list
854- const newCharms = filterOutEntity ( charms , id ) ;
855- if ( newCharms . length !== charms . get ( ) . length ) {
856- charms . set ( newCharms ) ;
857- }
858908
859- success = true ;
860- }
861- } ) ) && success ;
862- }
863-
864- // Permanently delete a charm (from trash or directly)
865- async permanentlyDelete ( idOrCharm : string | EntityId | Cell < unknown > ) {
866- let success ;
867-
868- await this . syncCharms ( this . trashedCharms ) ;
869-
870- const id = getEntityId ( idOrCharm ) ;
871- if ( ! id ) return false ;
872-
873- return ( ! await this . runtime . editWithRetry ( ( tx ) => {
874- // Remove from trash if present
875- const trashedCharms = this . trashedCharms . withTx ( tx ) ;
876- const newTrashedCharms = filterOutEntity ( trashedCharms , id ) ;
877- if ( newTrashedCharms . length !== trashedCharms . get ( ) . length ) {
878- trashedCharms . set ( newTrashedCharms ) ;
909+ // Remove from main list
910+ const newCharms = filterOutEntity ( charms , id ) ;
911+ if ( newCharms . length !== charms . get ( ) . length ) {
912+ charms . set ( newCharms ) ;
879913 success = true ;
880914 } else {
881915 success = false ;
0 commit comments