@graphprotocol/hypergraph 0.0.1

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.
Files changed (171) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +2 -0
  3. package/dist/connect/auth-storage.d.ts.map +1 -0
  4. package/dist/connect/create-app-identity.d.ts.map +1 -0
  5. package/dist/connect/create-auth-url.d.ts.map +1 -0
  6. package/dist/connect/create-auth-url.js +35 -0
  7. package/dist/connect/create-auth-url.js.map +1 -0
  8. package/dist/connect/create-callback-params.d.ts.map +1 -0
  9. package/dist/connect/create-callback-params.js +17 -0
  10. package/dist/connect/create-callback-params.js.map +1 -0
  11. package/dist/connect/create-identity-keys.d.ts.map +1 -0
  12. package/dist/connect/identity-encryption.d.ts.map +1 -0
  13. package/dist/connect/index.d.ts.map +1 -0
  14. package/dist/connect/login.d.ts.map +1 -0
  15. package/dist/connect/parse-callback-params.d.ts.map +1 -0
  16. package/dist/connect/parse-callback-params.js +63 -0
  17. package/dist/connect/parse-callback-params.js.map +1 -0
  18. package/dist/connect/prove-ownership.d.ts.map +1 -0
  19. package/dist/connect/types.d.ts +57 -0
  20. package/dist/connect/types.d.ts.map +1 -0
  21. package/dist/connect/types.js +24 -0
  22. package/dist/connect/types.js.map +1 -0
  23. package/dist/entity/create.d.ts.map +1 -0
  24. package/dist/entity/decodedEntitiesCache.d.ts.map +1 -0
  25. package/dist/entity/delete.d.ts.map +1 -0
  26. package/dist/entity/entity.d.ts.map +1 -0
  27. package/dist/entity/entityRelationParentsMap.d.ts.map +1 -0
  28. package/dist/entity/findMany.d.ts.map +1 -0
  29. package/dist/entity/findMany.js +436 -0
  30. package/dist/entity/findMany.js.map +1 -0
  31. package/dist/entity/findOne.d.ts.map +1 -0
  32. package/dist/entity/getEntityRelations.d.ts.map +1 -0
  33. package/dist/entity/index.d.ts.map +1 -0
  34. package/dist/entity/relationParentsMap.d.ts.map +1 -0
  35. package/dist/entity/removeRelation.d.ts.map +1 -0
  36. package/dist/entity/types.d.ts +79 -0
  37. package/dist/entity/types.d.ts.map +1 -0
  38. package/dist/entity/types.js +2 -0
  39. package/dist/entity/types.js.map +1 -0
  40. package/dist/entity/update.d.ts.map +1 -0
  41. package/dist/identity/auth-storage.d.ts.map +1 -0
  42. package/dist/identity/get-verified-identity.d.ts.map +1 -0
  43. package/dist/identity/identity-encryption.d.ts.map +1 -0
  44. package/dist/identity/index.d.ts.map +1 -0
  45. package/dist/identity/logout.d.ts.map +1 -0
  46. package/dist/identity/prove-ownership.d.ts.map +1 -0
  47. package/dist/inboxes/create-inbox.d.ts.map +1 -0
  48. package/dist/inboxes/get-list-inboxes.d.ts.map +1 -0
  49. package/dist/inboxes/index.d.ts.map +1 -0
  50. package/dist/inboxes/merge-messages.d.ts.map +1 -0
  51. package/dist/inboxes/message-encryption.d.ts.map +1 -0
  52. package/dist/inboxes/message-validation.d.ts.map +1 -0
  53. package/dist/inboxes/prepare-message.d.ts +31 -0
  54. package/dist/inboxes/prepare-message.d.ts.map +1 -0
  55. package/dist/inboxes/recover-inbox-creator.d.ts.map +1 -0
  56. package/dist/inboxes/recover-inbox-message-signer.d.ts.map +1 -0
  57. package/dist/inboxes/send-message.d.ts.map +1 -0
  58. package/dist/index.d.ts.map +1 -0
  59. package/dist/messages/index.d.ts.map +1 -0
  60. package/dist/messages/signed-update-message.d.ts.map +1 -0
  61. package/dist/messages/types.d.ts.map +1 -0
  62. package/dist/space-events/accept-invitation.d.ts.map +1 -0
  63. package/dist/space-events/apply-event.d.ts.map +1 -0
  64. package/dist/space-events/create-inbox.d.ts.map +1 -0
  65. package/dist/space-events/create-invitation.d.ts.map +1 -0
  66. package/dist/space-events/create-space.d.ts.map +1 -0
  67. package/dist/space-events/delete-space.d.ts.map +1 -0
  68. package/dist/space-events/hash-event.d.ts.map +1 -0
  69. package/dist/space-events/index.d.ts.map +1 -0
  70. package/dist/space-info/decrypt-space-info.d.ts.map +1 -0
  71. package/dist/space-info/encrypt-and-sign-space-info.d.ts.map +1 -0
  72. package/dist/space-info/index.d.ts.map +1 -0
  73. package/dist/store-connect.d.ts.map +1 -0
  74. package/dist/store.d.ts.map +1 -0
  75. package/dist/type/type.d.ts.map +1 -0
  76. package/dist/utils/automergeId.d.ts +9 -0
  77. package/dist/utils/automergeId.d.ts.map +1 -0
  78. package/dist/utils/automergeId.js +17 -0
  79. package/dist/utils/automergeId.js.map +1 -0
  80. package/dist/utils/generateId.d.ts +15 -0
  81. package/dist/utils/generateId.d.ts.map +1 -0
  82. package/dist/utils/generateId.js +18 -0
  83. package/dist/utils/generateId.js.map +1 -0
  84. package/dist/utils/index.d.ts.map +1 -0
  85. package/package.json +35 -0
  86. package/src/connect/auth-storage.ts +67 -0
  87. package/src/connect/create-app-identity.ts +16 -0
  88. package/src/connect/create-auth-url.ts +42 -0
  89. package/src/connect/create-callback-params.ts +30 -0
  90. package/src/connect/create-identity-keys.ts +20 -0
  91. package/src/connect/identity-encryption.ts +232 -0
  92. package/src/connect/index.ts +10 -0
  93. package/src/connect/login.ts +114 -0
  94. package/src/connect/parse-auth-params.ts +37 -0
  95. package/src/connect/parse-callback-params.ts +67 -0
  96. package/src/connect/prove-ownership.ts +58 -0
  97. package/src/connect/types.ts +67 -0
  98. package/src/entity/create.ts +58 -0
  99. package/src/entity/decodedEntitiesCache.ts +38 -0
  100. package/src/entity/delete.ts +52 -0
  101. package/src/entity/entity.ts +26 -0
  102. package/src/entity/entityRelationParentsMap.ts +6 -0
  103. package/src/entity/findMany.ts +506 -0
  104. package/src/entity/findOne.ts +34 -0
  105. package/src/entity/getEntityRelations.ts +45 -0
  106. package/src/entity/hasValidTypesProperty.ts +8 -0
  107. package/src/entity/index.ts +8 -0
  108. package/src/entity/relationParentsMap.ts +6 -0
  109. package/src/entity/removeRelation.ts +21 -0
  110. package/src/entity/test.ts +0 -0
  111. package/src/entity/types.ts +100 -0
  112. package/src/entity/update.ts +58 -0
  113. package/src/entity/variant-schema.ts +677 -0
  114. package/src/identity/auth-storage.ts +57 -0
  115. package/src/identity/get-verified-identity.ts +53 -0
  116. package/src/identity/identity-encryption.ts +140 -0
  117. package/src/identity/index.ts +6 -0
  118. package/src/identity/logout.ts +8 -0
  119. package/src/identity/prove-ownership.ts +58 -0
  120. package/src/identity/types.ts +44 -0
  121. package/src/inboxes/create-inbox.ts +102 -0
  122. package/src/inboxes/get-list-inboxes.ts +52 -0
  123. package/src/inboxes/index.ts +10 -0
  124. package/src/inboxes/merge-messages.ts +28 -0
  125. package/src/inboxes/message-encryption.ts +35 -0
  126. package/src/inboxes/message-validation.ts +66 -0
  127. package/src/inboxes/prepare-message.ts +85 -0
  128. package/src/inboxes/recover-inbox-creator.ts +29 -0
  129. package/src/inboxes/recover-inbox-message-signer.ts +42 -0
  130. package/src/inboxes/send-message.ts +75 -0
  131. package/src/inboxes/types.ts +9 -0
  132. package/src/index.ts +13 -0
  133. package/src/key/create-key.ts +27 -0
  134. package/src/key/decrypt-key.ts +19 -0
  135. package/src/key/encrypt-key.ts +27 -0
  136. package/src/key/index.ts +4 -0
  137. package/src/key/key-box.ts +31 -0
  138. package/src/messages/decrypt-message.ts +13 -0
  139. package/src/messages/encrypt-message.ts +14 -0
  140. package/src/messages/index.ts +5 -0
  141. package/src/messages/serialize.ts +24 -0
  142. package/src/messages/signed-update-message.ts +84 -0
  143. package/src/messages/types.ts +506 -0
  144. package/src/space-events/accept-invitation.ts +36 -0
  145. package/src/space-events/apply-event.ts +150 -0
  146. package/src/space-events/create-inbox.ts +56 -0
  147. package/src/space-events/create-invitation.ts +41 -0
  148. package/src/space-events/create-space.ts +35 -0
  149. package/src/space-events/delete-space.ts +36 -0
  150. package/src/space-events/hash-event.ts +10 -0
  151. package/src/space-events/index.ts +8 -0
  152. package/src/space-events/types.ts +137 -0
  153. package/src/space-info/decrypt-space-info.ts +22 -0
  154. package/src/space-info/encrypt-and-sign-space-info.ts +50 -0
  155. package/src/space-info/index.ts +3 -0
  156. package/src/space-info/types.ts +7 -0
  157. package/src/store-connect.ts +504 -0
  158. package/src/store.ts +493 -0
  159. package/src/type/type.ts +25 -0
  160. package/src/types.ts +47 -0
  161. package/src/utils/assertExhaustive.ts +3 -0
  162. package/src/utils/automergeId.ts +18 -0
  163. package/src/utils/base58.ts +74 -0
  164. package/src/utils/generateId.ts +18 -0
  165. package/src/utils/hexBytesAddressUtils.ts +25 -0
  166. package/src/utils/index.ts +8 -0
  167. package/src/utils/internal/base58Utils.ts +47 -0
  168. package/src/utils/internal/deep-merge.ts +38 -0
  169. package/src/utils/isRelationField.ts +9 -0
  170. package/src/utils/jsc.ts +94 -0
  171. package/src/utils/stringToUint8Array.ts +9 -0
@@ -0,0 +1,52 @@
1
+ import type { DocHandle } from '@automerge/automerge-repo';
2
+ import type { DocumentContent } from './types.js';
3
+
4
+ /**
5
+ * Deletes the exiting entity from the repo.
6
+ */
7
+ const delete$ = (handle: DocHandle<DocumentContent>) => {
8
+ return (id: string): boolean => {
9
+ let result = false;
10
+
11
+ // apply changes to the repo -> removes the existing entity by its id
12
+ handle.change((doc) => {
13
+ if (doc.entities?.[id] !== undefined) {
14
+ delete doc.entities[id];
15
+ result = true;
16
+ }
17
+ for (const [key, relation] of Object.entries(doc.relations ?? {})) {
18
+ if (doc.relations?.[key] && relation.from === id) {
19
+ delete doc.relations[key];
20
+ }
21
+ }
22
+ });
23
+
24
+ return result;
25
+ };
26
+ };
27
+
28
+ export { delete$ as delete };
29
+
30
+ /**
31
+ * Deletes the exiting entity from the repo.
32
+ */
33
+ export const markAsDeleted = (handle: DocHandle<DocumentContent>) => {
34
+ return (id: string): boolean => {
35
+ let result = false;
36
+
37
+ // apply changes to the repo -> removes the existing entity by its id
38
+ handle.change((doc) => {
39
+ if (doc.entities?.[id] !== undefined) {
40
+ doc.entities[id].__deleted = true;
41
+ result = true;
42
+ }
43
+ for (const [key, relation] of Object.entries(doc.relations ?? {})) {
44
+ if (doc.relations?.[key] && relation.from === id) {
45
+ doc.relations[key].__deleted = true;
46
+ }
47
+ }
48
+ });
49
+
50
+ return result;
51
+ };
52
+ };
@@ -0,0 +1,26 @@
1
+ import * as Data from 'effect/Data';
2
+ import type { AnyNoContext } from './types.js';
3
+ import * as VariantSchema from './variant-schema.js';
4
+
5
+ const {
6
+ Class,
7
+ Field,
8
+ // FieldExcept,
9
+ // FieldOnly,
10
+ // Struct,
11
+ // Union,
12
+ // extract,
13
+ // fieldEvolve,
14
+ // fieldFromKey
15
+ } = VariantSchema.make({
16
+ variants: ['select', 'insert', 'update'],
17
+ defaultVariant: 'select',
18
+ });
19
+
20
+ export { Class, Field };
21
+
22
+ export class EntityNotFoundError extends Data.TaggedError('EntityNotFoundError')<{
23
+ id: string;
24
+ type: AnyNoContext;
25
+ cause?: unknown;
26
+ }> {}
@@ -0,0 +1,6 @@
1
+ import type { DecodedEntitiesCacheEntry } from './decodedEntitiesCache.js';
2
+
3
+ export const entityRelationParentsMap: Map<
4
+ string, // entity ID
5
+ Map<DecodedEntitiesCacheEntry, number>
6
+ > = new Map();
@@ -0,0 +1,506 @@
1
+ import type { DocHandle, Patch } from '@automerge/automerge-repo';
2
+ import * as Schema from 'effect/Schema';
3
+ import { deepMerge } from '../utils/internal/deep-merge.js';
4
+ import { isRelationField } from '../utils/isRelationField.js';
5
+ import { canonicalize } from '../utils/jsc.js';
6
+ import { type DecodedEntitiesCacheEntry, type QueryEntry, decodedEntitiesCache } from './decodedEntitiesCache.js';
7
+ import { entityRelationParentsMap } from './entityRelationParentsMap.js';
8
+ import { getEntityRelations } from './getEntityRelations.js';
9
+ import { hasValidTypesProperty } from './hasValidTypesProperty.js';
10
+ import type {
11
+ AnyNoContext,
12
+ CrossFieldFilter,
13
+ DocumentContent,
14
+ Entity,
15
+ EntityFieldFilter,
16
+ EntityFilter,
17
+ EntityNumberFilter,
18
+ EntityTextFilter,
19
+ } from './types.js';
20
+
21
+ const documentChangeListener: {
22
+ subscribedQueriesCount: number;
23
+ unsubscribe: undefined | (() => void);
24
+ } = {
25
+ subscribedQueriesCount: 0,
26
+ unsubscribe: undefined,
27
+ };
28
+
29
+ const subscribeToDocumentChanges = (handle: DocHandle<DocumentContent>) => {
30
+ const onChange = ({ patches, doc }: { patches: Array<Patch>; doc: DocumentContent }) => {
31
+ const changedRelations = new Set<string>();
32
+ const deletedRelations = new Set<string>();
33
+ const changedEntities = new Set<string>();
34
+ const deletedEntities = new Set<string>();
35
+
36
+ for (const patch of patches) {
37
+ switch (patch.action) {
38
+ case 'put':
39
+ case 'insert':
40
+ case 'splice': {
41
+ if (patch.path.length > 2 && patch.path[0] === 'entities' && typeof patch.path[1] === 'string') {
42
+ changedEntities.add(patch.path[1]);
43
+ }
44
+ if (patch.path.length > 2 && patch.path[0] === 'relations' && typeof patch.path[1] === 'string') {
45
+ changedRelations.add(patch.path[1]);
46
+ }
47
+ break;
48
+ }
49
+ case 'del': {
50
+ if (patch.path.length === 2 && patch.path[0] === 'entities' && typeof patch.path[1] === 'string') {
51
+ deletedEntities.add(patch.path[1]);
52
+ }
53
+ if (patch.path.length === 2 && patch.path[0] === 'relations' && typeof patch.path[1] === 'string') {
54
+ deletedRelations.add(patch.path[1]);
55
+ }
56
+ break;
57
+ }
58
+ }
59
+ }
60
+
61
+ const entityTypes = new Set<string>();
62
+ // collect all query entries that changed and only at the end make one copy to change the
63
+ // reference to reduce the amount of O(n) operations per query to 1
64
+ const touchedQueries = new Set<Array<string>>();
65
+
66
+ // collect all entities that used this entity as a entry in on of their relation fields
67
+ const touchedRelationParents = new Set<DecodedEntitiesCacheEntry>();
68
+
69
+ // loop over all changed entities and update the cache
70
+ for (const entityId of changedEntities) {
71
+ const entity = doc.entities?.[entityId];
72
+ if (!hasValidTypesProperty(entity)) continue;
73
+ for (const typeName of entity['@@types@@']) {
74
+ if (typeof typeName !== 'string') continue;
75
+ const cacheEntry = decodedEntitiesCache.get(typeName);
76
+ if (!cacheEntry) continue;
77
+
78
+ let includeFromAllQueries = {};
79
+ for (const [, query] of cacheEntry.queries) {
80
+ includeFromAllQueries = deepMerge(includeFromAllQueries, query.include);
81
+ }
82
+
83
+ const oldDecodedEntry = cacheEntry.entities.get(entityId);
84
+ const relations = getEntityRelations(entityId, cacheEntry.type, doc, includeFromAllQueries);
85
+ let decoded: unknown | undefined;
86
+ try {
87
+ decoded = cacheEntry.decoder({
88
+ ...entity,
89
+ ...relations,
90
+ id: entityId,
91
+ });
92
+ cacheEntry.entities.set(entityId, decoded);
93
+ } catch (error) {
94
+ // TODO: store the corrupt entity ids somewhere, so they can be read via the API
95
+ console.error('error', error);
96
+ }
97
+
98
+ if (oldDecodedEntry) {
99
+ // collect all the Ids for relation entries in the `oldDecodedEntry`
100
+ const deletedRelationIds = new Set<string>();
101
+ for (const [, value] of Object.entries(oldDecodedEntry)) {
102
+ if (Array.isArray(value)) {
103
+ for (const relationEntity of value) {
104
+ deletedRelationIds.add(relationEntity.id);
105
+ }
106
+ }
107
+ }
108
+
109
+ // it's fine to remove all of them since they are re-added below
110
+ for (const deletedRelationId of deletedRelationIds) {
111
+ const deletedRelationEntry = entityRelationParentsMap.get(deletedRelationId);
112
+ if (deletedRelationEntry) {
113
+ deletedRelationEntry.set(cacheEntry, (deletedRelationEntry.get(cacheEntry) ?? 0) - 1);
114
+ if (deletedRelationEntry.get(cacheEntry) === 0) {
115
+ deletedRelationEntry.delete(cacheEntry);
116
+ }
117
+ if (deletedRelationEntry.size === 0) {
118
+ entityRelationParentsMap.delete(deletedRelationId);
119
+ }
120
+ }
121
+ }
122
+ }
123
+
124
+ if (decoded) {
125
+ for (const [, value] of Object.entries(decoded)) {
126
+ if (Array.isArray(value)) {
127
+ for (const relationEntity of value) {
128
+ let relationParentEntry = entityRelationParentsMap.get(relationEntity.id);
129
+ if (relationParentEntry) {
130
+ relationParentEntry.set(cacheEntry, (relationParentEntry.get(cacheEntry) ?? 0) + 1);
131
+ } else {
132
+ relationParentEntry = new Map();
133
+ entityRelationParentsMap.set(relationEntity.id, relationParentEntry);
134
+ relationParentEntry.set(cacheEntry, 1);
135
+ }
136
+ }
137
+ }
138
+ }
139
+ }
140
+
141
+ for (const [queryKey, query] of cacheEntry.queries) {
142
+ touchedQueries.add([typeName, queryKey]);
143
+ query.isInvalidated = true;
144
+ }
145
+
146
+ entityTypes.add(typeName);
147
+
148
+ // gather all the decodedEntitiesCacheEntries that have a relation to this entity to
149
+ // invoke their query listeners below
150
+ if (entityRelationParentsMap.has(entityId)) {
151
+ const decodedEntitiesCacheEntries = entityRelationParentsMap.get(entityId);
152
+ if (!decodedEntitiesCacheEntries) return;
153
+
154
+ for (const [entry] of decodedEntitiesCacheEntries) {
155
+ touchedRelationParents.add(entry);
156
+ }
157
+ }
158
+ }
159
+ }
160
+
161
+ // loop over all deleted entities and remove them from the cache
162
+ for (const entityId of deletedEntities) {
163
+ for (const [affectedTypeName, cacheEntry] of decodedEntitiesCache) {
164
+ if (cacheEntry.entities.has(entityId)) {
165
+ entityTypes.add(affectedTypeName);
166
+ cacheEntry.entities.delete(entityId);
167
+
168
+ for (const [queryKey, query] of cacheEntry.queries) {
169
+ // find the entity in the query and remove it using splice
170
+ const index = query.data.findIndex((entity) => entity.id === entityId);
171
+ if (index !== -1) {
172
+ query.data.splice(index, 1);
173
+ touchedQueries.add([affectedTypeName, queryKey]);
174
+ }
175
+ }
176
+ }
177
+ }
178
+
179
+ // gather all the queries of impacted parent relation queries and then remove the cacheEntry
180
+ if (entityRelationParentsMap.has(entityId)) {
181
+ const decodedEntitiesCacheEntries = entityRelationParentsMap.get(entityId);
182
+ if (!decodedEntitiesCacheEntries) return;
183
+
184
+ for (const [entry] of decodedEntitiesCacheEntries) {
185
+ touchedRelationParents.add(entry);
186
+ }
187
+
188
+ entityRelationParentsMap.delete(entityId);
189
+ }
190
+ }
191
+
192
+ // update the queries affected queries
193
+ for (const [typeName, queryKey] of touchedQueries) {
194
+ const cacheEntry = decodedEntitiesCache.get(typeName);
195
+ if (!cacheEntry) continue;
196
+
197
+ const query = cacheEntry.queries.get(queryKey);
198
+ if (!query) continue;
199
+
200
+ query.data = [...query.data]; // must be a new reference for React.useSyncExternalStore
201
+ }
202
+
203
+ // invoke all the listeners per type
204
+ for (const typeName of entityTypes) {
205
+ const cacheEntry = decodedEntitiesCache.get(typeName);
206
+ if (!cacheEntry) continue;
207
+
208
+ for (const query of cacheEntry.queries.values()) {
209
+ for (const listener of query.listeners) {
210
+ listener();
211
+ }
212
+ }
213
+ }
214
+
215
+ // trigger all the listeners of the parent relation queries
216
+ for (const decodedEntitiesCacheEntry of touchedRelationParents) {
217
+ decodedEntitiesCacheEntry.isInvalidated = true;
218
+ for (const query of decodedEntitiesCacheEntry.queries.values()) {
219
+ query.isInvalidated = true;
220
+ for (const listener of query.listeners) {
221
+ listener();
222
+ }
223
+ }
224
+ }
225
+ };
226
+
227
+ handle.on('change', onChange);
228
+
229
+ return () => {
230
+ handle.off('change', onChange);
231
+ decodedEntitiesCache.clear(); // currently we only support exactly one space
232
+ };
233
+ };
234
+
235
+ /**
236
+ * Queries for a list of entities of the given type from the repo.
237
+ */
238
+ export function findMany<const S extends AnyNoContext>(
239
+ handle: DocHandle<DocumentContent>,
240
+ type: S,
241
+ filter: EntityFilter<Schema.Schema.Type<S>> | undefined,
242
+ include: { [K in keyof Schema.Schema.Type<S>]?: Record<string, never> } | undefined,
243
+ ): { entities: Readonly<Array<Entity<S>>>; corruptEntityIds: Readonly<Array<string>> } {
244
+ const decode = Schema.decodeUnknownSync(type);
245
+ // TODO: what's the right way to get the name of the type?
246
+ // @ts-expect-error name is defined
247
+ const typeName = type.name;
248
+
249
+ const doc = handle.doc();
250
+ if (!doc) {
251
+ return { entities: [], corruptEntityIds: [] };
252
+ }
253
+ const entities = doc.entities ?? {};
254
+ const corruptEntityIds: string[] = [];
255
+ const filtered: Array<Entity<S>> = [];
256
+
257
+ const evaluateFilter = <T>(fieldFilter: EntityFieldFilter<T>, fieldValue: T): boolean => {
258
+ // Handle NOT operator
259
+ if ('NOT' in fieldFilter && fieldFilter.NOT) {
260
+ return !evaluateFilter(fieldFilter.NOT, fieldValue);
261
+ }
262
+
263
+ // Handle OR operator
264
+ if ('OR' in fieldFilter) {
265
+ const orFilters = fieldFilter.OR;
266
+ if (Array.isArray(orFilters)) {
267
+ return orFilters.some((orFilter) => evaluateFilter(orFilter as EntityFieldFilter<T>, fieldValue));
268
+ }
269
+ }
270
+
271
+ // Handle basic filters
272
+ if ('is' in fieldFilter) {
273
+ if (typeof fieldValue === 'boolean') {
274
+ return fieldValue === fieldFilter.is;
275
+ }
276
+ if (typeof fieldValue === 'number') {
277
+ return fieldValue === fieldFilter.is;
278
+ }
279
+ if (typeof fieldValue === 'string') {
280
+ return fieldValue === fieldFilter.is;
281
+ }
282
+ }
283
+
284
+ if (typeof fieldValue === 'number') {
285
+ if ('greaterThan' in fieldFilter) {
286
+ const numberFilter = fieldFilter as EntityNumberFilter;
287
+ if (numberFilter.greaterThan !== undefined && fieldValue <= numberFilter.greaterThan) {
288
+ return false;
289
+ }
290
+ }
291
+ if ('lessThan' in fieldFilter) {
292
+ const numberFilter = fieldFilter as EntityNumberFilter;
293
+ if (numberFilter.lessThan !== undefined && fieldValue >= numberFilter.lessThan) {
294
+ return false;
295
+ }
296
+ }
297
+ }
298
+
299
+ if (typeof fieldValue === 'string') {
300
+ if ('startsWith' in fieldFilter) {
301
+ const textFilter = fieldFilter as EntityTextFilter;
302
+ if (textFilter.startsWith !== undefined && !fieldValue.startsWith(textFilter.startsWith)) {
303
+ return false;
304
+ }
305
+ }
306
+ if ('endsWith' in fieldFilter) {
307
+ const textFilter = fieldFilter as EntityTextFilter;
308
+ if (textFilter.endsWith !== undefined && !fieldValue.endsWith(textFilter.endsWith)) {
309
+ return false;
310
+ }
311
+ }
312
+ if ('contains' in fieldFilter) {
313
+ const textFilter = fieldFilter as EntityTextFilter;
314
+ if (textFilter.contains !== undefined && !fieldValue.includes(textFilter.contains)) {
315
+ return false;
316
+ }
317
+ }
318
+ }
319
+
320
+ return true;
321
+ };
322
+
323
+ const evaluateCrossFieldFilter = (
324
+ crossFieldFilter: CrossFieldFilter<Schema.Schema.Type<S>>,
325
+ entity: Entity<S>,
326
+ ): boolean => {
327
+ for (const fieldName in crossFieldFilter) {
328
+ const fieldFilter = crossFieldFilter[fieldName];
329
+ const fieldValue = entity[fieldName];
330
+
331
+ if (fieldFilter && !evaluateFilter(fieldFilter, fieldValue)) {
332
+ return false;
333
+ }
334
+ }
335
+ return true;
336
+ };
337
+
338
+ const evaluateEntityFilter = (entityFilter: EntityFilter<Schema.Schema.Type<S>>, entity: Entity<S>): boolean => {
339
+ // handle top-level NOT operator
340
+ if ('NOT' in entityFilter && entityFilter.NOT) {
341
+ return !evaluateCrossFieldFilter(entityFilter.NOT, entity);
342
+ }
343
+
344
+ // handle top-level OR operator
345
+ if ('OR' in entityFilter && Array.isArray(entityFilter.OR)) {
346
+ return entityFilter.OR.some((orFilter) => evaluateCrossFieldFilter(orFilter, entity));
347
+ }
348
+
349
+ // evaluate regular field filters
350
+ return evaluateCrossFieldFilter(entityFilter, entity);
351
+ };
352
+
353
+ for (const id in entities) {
354
+ const entity = entities[id];
355
+ if (hasValidTypesProperty(entity) && entity['@@types@@'].includes(typeName)) {
356
+ const relations = getEntityRelations(id, type, doc, include);
357
+ try {
358
+ const decoded = { ...decode({ ...entity, ...relations, id }), type: typeName };
359
+ if (filter) {
360
+ if (evaluateEntityFilter(filter, decoded)) {
361
+ filtered.push(decoded);
362
+ }
363
+ } else {
364
+ filtered.push(decoded);
365
+ }
366
+ } catch (error) {
367
+ corruptEntityIds.push(id);
368
+ }
369
+ }
370
+ }
371
+
372
+ return { entities: filtered, corruptEntityIds: [] };
373
+ }
374
+
375
+ const stableEmptyArray: Array<unknown> = [];
376
+
377
+ export function subscribeToFindMany<const S extends AnyNoContext>(
378
+ handle: DocHandle<DocumentContent>,
379
+ type: S,
380
+ filter: { [K in keyof Schema.Schema.Type<S>]?: EntityFieldFilter<Schema.Schema.Type<S>[K]> } | undefined,
381
+ include: { [K in keyof Schema.Schema.Type<S>]?: Record<string, never> } | undefined,
382
+ ): {
383
+ subscribe: (callback: () => void) => () => void;
384
+ getEntities: () => Readonly<Array<Entity<S>>>;
385
+ } {
386
+ const queryKey = filter ? canonicalize(filter) : 'all';
387
+ const decode = Schema.decodeUnknownSync(type);
388
+ // TODO: what's the right way to get the name of the type?
389
+ // @ts-expect-error name is defined
390
+ const typeName = type.name;
391
+
392
+ const getEntities = () => {
393
+ const cacheEntry = decodedEntitiesCache.get(typeName);
394
+ if (!cacheEntry) return stableEmptyArray;
395
+
396
+ const query = cacheEntry.queries.get(queryKey);
397
+ if (!query) return stableEmptyArray;
398
+
399
+ if (!cacheEntry.isInvalidated && !query.isInvalidated) {
400
+ return query.data;
401
+ }
402
+
403
+ const { entities } = findMany(handle, type, filter, include);
404
+
405
+ for (const entity of entities) {
406
+ cacheEntry?.entities.set(entity.id, entity);
407
+ }
408
+
409
+ // must be a new reference to ensure it can be used in React.useMemo
410
+ query.data = [...entities];
411
+ cacheEntry.isInvalidated = false;
412
+ query.isInvalidated = false;
413
+
414
+ return query.data;
415
+ };
416
+
417
+ const allTypes = new Set<S>();
418
+ for (const [_key, field] of Object.entries(type.fields)) {
419
+ if (isRelationField(field)) {
420
+ allTypes.add(field as S);
421
+ }
422
+ }
423
+
424
+ const subscribe = (callback: () => void) => {
425
+ let cacheEntry = decodedEntitiesCache.get(typeName);
426
+
427
+ if (!cacheEntry) {
428
+ const entitiesMap = new Map();
429
+ const queries = new Map<string, QueryEntry>();
430
+
431
+ queries.set(queryKey, {
432
+ data: [],
433
+ listeners: [],
434
+ isInvalidated: true,
435
+ include: include ?? {},
436
+ });
437
+
438
+ cacheEntry = {
439
+ decoder: decode,
440
+ type,
441
+ entities: entitiesMap,
442
+ queries,
443
+ isInvalidated: true,
444
+ };
445
+
446
+ decodedEntitiesCache.set(typeName, cacheEntry);
447
+ }
448
+
449
+ let query = cacheEntry.queries.get(queryKey);
450
+ if (!query) {
451
+ query = {
452
+ data: [],
453
+ listeners: [],
454
+ isInvalidated: true,
455
+ include: include ?? {},
456
+ };
457
+ // we just set up the query and expect it to correctly set itself up in findMany
458
+ cacheEntry.queries.set(queryKey, query);
459
+ }
460
+
461
+ if (query?.listeners) {
462
+ query.listeners.push(callback);
463
+ }
464
+
465
+ return () => {
466
+ const cacheEntry = decodedEntitiesCache.get(typeName);
467
+ if (cacheEntry) {
468
+ // first cleanup the queries
469
+ const query = cacheEntry.queries.get(queryKey);
470
+ if (query) {
471
+ query.listeners = query?.listeners?.filter((cachedListener) => cachedListener !== callback);
472
+ if (query.listeners.length === 0) {
473
+ cacheEntry.queries.delete(queryKey);
474
+ }
475
+ }
476
+ // if the last query is removed, cleanup the entityRelationParentsMap and remove the decodedEntitiesCacheEntry
477
+ if (cacheEntry.queries.size === 0) {
478
+ entityRelationParentsMap.forEach((relationCacheEntries, key) => {
479
+ for (const [relationCacheEntry, counter] of relationCacheEntries) {
480
+ if (relationCacheEntry === cacheEntry && counter === 0) {
481
+ relationCacheEntries.delete(cacheEntry);
482
+ }
483
+ }
484
+ if (relationCacheEntries.size === 0) {
485
+ entityRelationParentsMap.delete(key);
486
+ }
487
+ });
488
+ decodedEntitiesCache.delete(typeName);
489
+ }
490
+ }
491
+
492
+ documentChangeListener.subscribedQueriesCount--;
493
+ if (documentChangeListener.subscribedQueriesCount === 0) {
494
+ documentChangeListener.unsubscribe?.();
495
+ documentChangeListener.unsubscribe = undefined;
496
+ }
497
+ };
498
+ };
499
+
500
+ if (documentChangeListener.subscribedQueriesCount === 0) {
501
+ documentChangeListener.unsubscribe = subscribeToDocumentChanges(handle);
502
+ }
503
+ documentChangeListener.subscribedQueriesCount++;
504
+
505
+ return { subscribe, getEntities };
506
+ }
@@ -0,0 +1,34 @@
1
+ import type { DocHandle } from '@automerge/automerge-repo';
2
+ import * as Schema from 'effect/Schema';
3
+ import { getEntityRelations } from './getEntityRelations.js';
4
+ import { hasValidTypesProperty } from './hasValidTypesProperty.js';
5
+ import type { AnyNoContext, DocumentContent, Entity } from './types.js';
6
+
7
+ /**
8
+ * Find the entity of the given type, with the given id, from the repo.
9
+ */
10
+ export const findOne = <const S extends AnyNoContext>(
11
+ handle: DocHandle<DocumentContent>,
12
+ type: S,
13
+ include: { [K in keyof Schema.Schema.Type<S>]?: Record<string, never> } = {},
14
+ ) => {
15
+ const decode = Schema.decodeUnknownSync(type);
16
+
17
+ // TODO: what's the right way to get the name of the type?
18
+ // @ts-expect-error name is defined
19
+ const typeName = type.name;
20
+
21
+ return (id: string): Entity<S> | undefined => {
22
+ // TODO: Instead of this insane filtering logic, we should be keeping track of the entities in
23
+ // an index and store the decoded values instead of re-decoding over and over again.
24
+ const doc = handle.doc();
25
+ const entity = doc?.entities?.[id];
26
+ const relations = doc ? getEntityRelations(id, type, doc, include) : {};
27
+
28
+ if (hasValidTypesProperty(entity) && entity['@@types@@'].includes(typeName)) {
29
+ return { ...decode({ ...entity, id, ...relations }), type: typeName };
30
+ }
31
+
32
+ return undefined;
33
+ };
34
+ };
@@ -0,0 +1,45 @@
1
+ import type * as Schema from 'effect/Schema';
2
+ import { isRelationField } from '../utils/isRelationField.js';
3
+ import { hasValidTypesProperty } from './hasValidTypesProperty.js';
4
+ import type { AnyNoContext, DocumentContent, Entity } from './types.js';
5
+ export const getEntityRelations = <const S extends AnyNoContext>(
6
+ entityId: string,
7
+ type: S,
8
+ doc: DocumentContent,
9
+ include: { [K in keyof Schema.Schema.Type<S>]?: Record<string, never> } | undefined,
10
+ ) => {
11
+ const relations: Record<string, Entity<AnyNoContext>> = {};
12
+ for (const [fieldName, field] of Object.entries(type.fields)) {
13
+ // skip non-relation fields or relations that are not defined in the include object
14
+ if (!isRelationField(field)) continue;
15
+
16
+ // Currently we still add an empty array for relations that are not included.
17
+ // This is to ensure that the relation is not undefined in the decoded entity.
18
+ // In the future we might want to derive a schema based on the include object.
19
+ if (!include?.[fieldName]) {
20
+ relations[fieldName] = [];
21
+ continue;
22
+ }
23
+
24
+ const relationEntities: Array<Entity<AnyNoContext>> = [];
25
+
26
+ for (const [relationId, relation] of Object.entries(doc.relations ?? {})) {
27
+ // @ts-expect-error name is defined
28
+ const typeName = type.name;
29
+
30
+ if (relation.fromTypeName !== typeName || relation.fromPropertyName !== fieldName || relation.from !== entityId)
31
+ continue;
32
+
33
+ if (relation.__deleted) continue;
34
+
35
+ const relationEntity = doc.entities?.[relation.to];
36
+ if (!hasValidTypesProperty(relationEntity)) continue;
37
+
38
+ relationEntities.push({ ...relationEntity, id: relation.to, _relation: { id: relationId } });
39
+ }
40
+
41
+ relations[fieldName] = relationEntities;
42
+ }
43
+
44
+ return relations;
45
+ };
@@ -0,0 +1,8 @@
1
+ export const hasValidTypesProperty = (value: unknown): value is Record<'@@types@@', unknown[]> => {
2
+ return (
3
+ value !== null &&
4
+ typeof value === 'object' &&
5
+ '@@types@@' in value &&
6
+ Array.isArray((value as { '@@types@@': unknown })['@@types@@'])
7
+ );
8
+ };
@@ -0,0 +1,8 @@
1
+ export * from './create.js';
2
+ export * from './delete.js';
3
+ export * from './entity.js';
4
+ export * from './findMany.js';
5
+ export * from './findOne.js';
6
+ export * from './removeRelation.js';
7
+ export * from './types.js';
8
+ export * from './update.js';
@@ -0,0 +1,6 @@
1
+ import type { DecodedEntitiesCacheEntry } from './decodedEntitiesCache.js';
2
+
3
+ export const relationParentsMap: Map<
4
+ string, // relation Id
5
+ Map<DecodedEntitiesCacheEntry, number>
6
+ > = new Map();