1- import type { DocHandle } from '@automerge/automerge-repo' ;
1+ import type { DocHandle , Patch } from '@automerge/automerge-repo' ;
22import * as VariantSchema from '@effect/experimental/VariantSchema' ;
33import * as Data from 'effect/Data' ;
44import * as Schema from 'effect/Schema' ;
@@ -66,6 +66,132 @@ export class EntityNotFoundError extends Data.TaggedError('EntityNotFoundError')
6666
6767export type Entity < S extends AnyNoContext > = Schema . Schema . Type < S > & { type : string } ;
6868
69+ /*
70+ * Note: Currently we only use one global cache for all entities.
71+ * In the future we probably want a build function that creates a cache and returns the
72+ * functions (create, update, findMany, …) that use this specific cache.
73+ *
74+ * How does it work?
75+ *
76+ * We store all decoded entities in a cache and for each query we reference the entities relevant to this query.
77+ * Whenever a query is registered we add it to the cache and set the reference count to 1. Whenever a query is unregistered
78+ * we decrease the reference count. Whenever the reference count reaches 0 we remove the query from the cache and remove the
79+ * entities from the cache.
80+ *
81+ * Handling filters is relatively straight forward as they are uniquely identified by their params.
82+ *
83+ * Questions:
84+ * How do we handle findOne?
85+ * Thoughts: Could be just a special case of findMany limited to a specific id or a separate mechanism.
86+ * How do we handle relations?
87+ * Thoughts: We could have a separate query entry for each relation, but when requesting a lot of entities e.g. 1000 only for a nesting one level deep it would result in a lot of cached queries. Not sure this is a good idea.
88+ */
89+ type DecodedEntitiesCache = Map <
90+ string , // type name
91+ {
92+ decoder : ( data : unknown ) => unknown ;
93+ entities : Map < string , Entity < AnyNoContext > > ; // holds all entities of this type
94+ queries : Map <
95+ string , // instead of serializedQueryKey we could also have the actual params
96+ {
97+ data : Array < Entity < AnyNoContext > > ; // holds the decoded entities of this query and must be a stable reference and use the same reference for the `entities` array
98+ listeners : Array < ( ) => void > ; // listeners to this query
99+ }
100+ > ;
101+ }
102+ > ;
103+
104+ const decodedEntitiesCache : DecodedEntitiesCache = new Map ( ) ;
105+
106+ export const registerChangeListener = ( handle : DocHandle < DocumentContent > ) => {
107+ const onChange = ( { patches, doc } : { patches : Array < Patch > ; doc : DocumentContent } ) => {
108+ const changedEntities = new Set < string > ( ) ;
109+ const deletedEntities = new Set < string > ( ) ;
110+
111+ for ( const patch of patches ) {
112+ switch ( patch . action ) {
113+ case 'put' :
114+ case 'insert' :
115+ case 'splice' : {
116+ if ( patch . path . length > 2 && patch . path [ 0 ] === 'entities' && typeof patch . path [ 1 ] === 'string' ) {
117+ changedEntities . add ( patch . path [ 1 ] ) ;
118+ }
119+ break ;
120+ }
121+ case 'del' : {
122+ if ( patch . path . length === 2 && patch . path [ 0 ] === 'entities' && typeof patch . path [ 1 ] === 'string' ) {
123+ deletedEntities . add ( patch . path [ 1 ] ) ;
124+ }
125+ break ;
126+ }
127+ }
128+ }
129+
130+ const entityTypes = new Set < string > ( ) ;
131+
132+ // loop over all changed entities and update the cache
133+ for ( const entityId of changedEntities ) {
134+ const entity = doc . entities ?. [ entityId ] ;
135+ if ( ! entity || typeof entity !== 'object' || ! ( '@@types@@' in entity ) || ! Array . isArray ( entity [ '@@types@@' ] ) )
136+ return ;
137+ for ( const typeName of entity [ '@@types@@' ] ) {
138+ const cacheEntry = decodedEntitiesCache . get ( typeName ) ;
139+ if ( ! cacheEntry ) continue ;
140+
141+ const decoded = cacheEntry . decoder ( { ...entity , id : entityId } ) ;
142+ cacheEntry . entities . set ( entityId , decoded ) ;
143+
144+ const query = cacheEntry . queries . get ( 'all' ) ;
145+ if ( query ) {
146+ const index = query . data . findIndex ( ( entity ) => entity . id === entityId ) ;
147+ if ( index !== - 1 ) {
148+ query . data [ index ] = decoded ;
149+ } else {
150+ query . data . push ( decoded ) ;
151+ }
152+ }
153+
154+ entityTypes . add ( typeName ) ;
155+ }
156+ }
157+
158+ // loop over all deleted entities and remove them from the cache
159+ for ( const entityId of deletedEntities ) {
160+ for ( const [ affectedTypeName , cacheEntry ] of decodedEntitiesCache ) {
161+ if ( cacheEntry . entities . has ( entityId ) ) {
162+ entityTypes . add ( affectedTypeName ) ;
163+ cacheEntry . entities . delete ( entityId ) ;
164+
165+ for ( const [ , query ] of cacheEntry . queries ) {
166+ // find the entity in the query and remove it using splice
167+ const index = query . data . findIndex ( ( entity ) => entity . id === entityId ) ;
168+ if ( index !== - 1 ) {
169+ query . data . splice ( index , 1 ) ;
170+ }
171+ }
172+ }
173+ }
174+ }
175+
176+ for ( const typeName of entityTypes ) {
177+ const cacheEntry = decodedEntitiesCache . get ( typeName ) ;
178+ if ( ! cacheEntry ) continue ;
179+
180+ for ( const query of cacheEntry . queries . values ( ) ) {
181+ for ( const listener of query . listeners ) {
182+ listener ( ) ;
183+ }
184+ }
185+ }
186+ } ;
187+
188+ handle . on ( 'change' , onChange ) ;
189+
190+ return ( ) => {
191+ handle . off ( 'change' , onChange ) ;
192+ } ;
193+ } ;
194+
69195/**
70196 * Creates an entity model of given type and stores it in the repo.
71197 */
@@ -78,7 +204,7 @@ export const create = <const S extends AnyNoContext>(handle: DocHandle<DocumentC
78204
79205 return ( data : Readonly < Schema . Schema . Type < Insert < S > > > ) : Entity < S > => {
80206 const encoded = encode ( data ) ;
81- // apply changes to the repo -> adds the entity to the repo entites document
207+ // apply changes to the repo -> adds the entity to the repo entities document
82208 handle . change ( ( doc ) => {
83209 doc . entities ??= { } ;
84210 doc . entities [ entityId ] = { ...encoded , '@@types@@' : [ typeName ] } ;
@@ -103,7 +229,7 @@ export const update = <const S extends AnyNoContext>(handle: DocHandle<DocumentC
103229 return ( id : string , data : Schema . Simplify < Partial < Schema . Schema . Type < Update < S > > > > ) : Entity < S > => {
104230 validate ( data ) ;
105231
106- // apply changes to the repo -> updates the existing entity to the repo entites document
232+ // apply changes to the repo -> updates the existing entity to the repo entities document
107233 let updated : Schema . Schema . Type < S > | undefined = undefined ;
108234 handle . change ( ( doc ) => {
109235 if ( doc . entities === undefined ) {
@@ -116,7 +242,7 @@ export const update = <const S extends AnyNoContext>(handle: DocHandle<DocumentC
116242 return ;
117243 }
118244
119- // TODO: Try to get a diff of the entitiy properties and only override the changed ones.
245+ // TODO: Try to get a diff of the entity properties and only override the changed ones.
120246 updated = { ...decode ( entity ) , ...data } ;
121247 doc . entities [ id ] = { ...encode ( updated ) , '@@types@@' : [ typeName ] } ;
122248 } ) ;
@@ -163,7 +289,7 @@ export function findMany<const S extends AnyNoContext>(
163289 const typeName = type . name ;
164290
165291 // TODO: Instead of this insane filtering logic, we should be keeping track of the entities in
166- // an index and store the decoded valeus instead of re-decoding over and over again.
292+ // an index and store the decoded values instead of re-decoding over and over again.
167293 const entities = handle . docSync ( ) ?. entities ?? { } ;
168294 const filtered : Array < Entity < S > > = [ ] ;
169295 for ( const id in entities ) {
@@ -179,6 +305,72 @@ export function findMany<const S extends AnyNoContext>(
179305 return filtered ;
180306}
181307
308+ export function subscribeToFindMany < const S extends AnyNoContext > (
309+ handle : DocHandle < DocumentContent > ,
310+ type : S ,
311+ ) : { listener : ( ) => ( ) => void ; getEntities : ( ) => Readonly < Array < Entity < S > > > } {
312+ const decode = Schema . decodeUnknownSync ( type ) ;
313+ // TODO: what's the right way to get the name of the type?
314+ // @ts -expect-error name is defined
315+ const typeName = type . name ;
316+
317+ const getEntities = ( ) => {
318+ const entities = decodedEntitiesCache . get ( typeName ) ?. queries . get ( 'all' ) ?. data ?? [ ] ;
319+ return entities ;
320+ } ;
321+
322+ const listener = ( ) => {
323+ return ( ) => undefined ;
324+ } ;
325+
326+ const entities = findMany ( handle , type ) ;
327+
328+ if ( decodedEntitiesCache . has ( typeName ) ) {
329+ // add a listener to the existing query
330+ const cacheEntry = decodedEntitiesCache . get ( typeName ) ;
331+ const query = cacheEntry ?. queries . get ( 'all' ) ;
332+
333+ for ( const entity of entities ) {
334+ cacheEntry ?. entities . set ( entity . id , entity ) ;
335+
336+ if ( ! query ) continue ;
337+
338+ const index = query . data . findIndex ( ( e ) => e . id === entity . id ) ;
339+ if ( index !== - 1 ) {
340+ query . data [ index ] = entity ;
341+ } else {
342+ query . data . push ( entity ) ;
343+ }
344+ }
345+
346+ if ( query ?. listeners ) {
347+ query . listeners . push ( listener ) ;
348+ }
349+ } else {
350+ const entitiesMap = new Map ( ) ;
351+ for ( const entity of entities ) {
352+ entitiesMap . set ( entity . id , entity ) ;
353+ }
354+
355+ const queries = new Map ( ) ;
356+
357+ queries . set ( 'all' , {
358+ data : entities ,
359+ listeners : [ listener ] ,
360+ } ) ;
361+
362+ decodedEntitiesCache . set ( typeName , {
363+ decoder : decode ,
364+ entities : entitiesMap ,
365+ queries,
366+ } ) ;
367+ }
368+
369+ // TODO when switching from one to another space the cache must be wiped
370+ // TODO also return an unsubscribe function to remove the listener from the cache
371+ return { listener, getEntities } ;
372+ }
373+
182374/**
183375 * Find the entity of the given type, with the given id, from the repo.
184376 */
@@ -192,7 +384,7 @@ export const findOne =
192384 const typeName = type . name ;
193385
194386 // TODO: Instead of this insane filtering logic, we should be keeping track of the entities in
195- // an index and store the decoded valeus instead of re-decoding over and over again.
387+ // an index and store the decoded values instead of re-decoding over and over again.
196388 const entity = handle . docSync ( ) ?. entities ?. [ id ] ;
197389 if ( typeof entity === 'object' && entity != null && '@@types@@' in entity ) {
198390 const types = entity [ '@@types@@' ] ;
0 commit comments