@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.
- package/LICENSE +21 -0
- package/README.md +2 -0
- package/dist/connect/auth-storage.d.ts.map +1 -0
- package/dist/connect/create-app-identity.d.ts.map +1 -0
- package/dist/connect/create-auth-url.d.ts.map +1 -0
- package/dist/connect/create-auth-url.js +35 -0
- package/dist/connect/create-auth-url.js.map +1 -0
- package/dist/connect/create-callback-params.d.ts.map +1 -0
- package/dist/connect/create-callback-params.js +17 -0
- package/dist/connect/create-callback-params.js.map +1 -0
- package/dist/connect/create-identity-keys.d.ts.map +1 -0
- package/dist/connect/identity-encryption.d.ts.map +1 -0
- package/dist/connect/index.d.ts.map +1 -0
- package/dist/connect/login.d.ts.map +1 -0
- package/dist/connect/parse-callback-params.d.ts.map +1 -0
- package/dist/connect/parse-callback-params.js +63 -0
- package/dist/connect/parse-callback-params.js.map +1 -0
- package/dist/connect/prove-ownership.d.ts.map +1 -0
- package/dist/connect/types.d.ts +57 -0
- package/dist/connect/types.d.ts.map +1 -0
- package/dist/connect/types.js +24 -0
- package/dist/connect/types.js.map +1 -0
- package/dist/entity/create.d.ts.map +1 -0
- package/dist/entity/decodedEntitiesCache.d.ts.map +1 -0
- package/dist/entity/delete.d.ts.map +1 -0
- package/dist/entity/entity.d.ts.map +1 -0
- package/dist/entity/entityRelationParentsMap.d.ts.map +1 -0
- package/dist/entity/findMany.d.ts.map +1 -0
- package/dist/entity/findMany.js +436 -0
- package/dist/entity/findMany.js.map +1 -0
- package/dist/entity/findOne.d.ts.map +1 -0
- package/dist/entity/getEntityRelations.d.ts.map +1 -0
- package/dist/entity/index.d.ts.map +1 -0
- package/dist/entity/relationParentsMap.d.ts.map +1 -0
- package/dist/entity/removeRelation.d.ts.map +1 -0
- package/dist/entity/types.d.ts +79 -0
- package/dist/entity/types.d.ts.map +1 -0
- package/dist/entity/types.js +2 -0
- package/dist/entity/types.js.map +1 -0
- package/dist/entity/update.d.ts.map +1 -0
- package/dist/identity/auth-storage.d.ts.map +1 -0
- package/dist/identity/get-verified-identity.d.ts.map +1 -0
- package/dist/identity/identity-encryption.d.ts.map +1 -0
- package/dist/identity/index.d.ts.map +1 -0
- package/dist/identity/logout.d.ts.map +1 -0
- package/dist/identity/prove-ownership.d.ts.map +1 -0
- package/dist/inboxes/create-inbox.d.ts.map +1 -0
- package/dist/inboxes/get-list-inboxes.d.ts.map +1 -0
- package/dist/inboxes/index.d.ts.map +1 -0
- package/dist/inboxes/merge-messages.d.ts.map +1 -0
- package/dist/inboxes/message-encryption.d.ts.map +1 -0
- package/dist/inboxes/message-validation.d.ts.map +1 -0
- package/dist/inboxes/prepare-message.d.ts +31 -0
- package/dist/inboxes/prepare-message.d.ts.map +1 -0
- package/dist/inboxes/recover-inbox-creator.d.ts.map +1 -0
- package/dist/inboxes/recover-inbox-message-signer.d.ts.map +1 -0
- package/dist/inboxes/send-message.d.ts.map +1 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/messages/index.d.ts.map +1 -0
- package/dist/messages/signed-update-message.d.ts.map +1 -0
- package/dist/messages/types.d.ts.map +1 -0
- package/dist/space-events/accept-invitation.d.ts.map +1 -0
- package/dist/space-events/apply-event.d.ts.map +1 -0
- package/dist/space-events/create-inbox.d.ts.map +1 -0
- package/dist/space-events/create-invitation.d.ts.map +1 -0
- package/dist/space-events/create-space.d.ts.map +1 -0
- package/dist/space-events/delete-space.d.ts.map +1 -0
- package/dist/space-events/hash-event.d.ts.map +1 -0
- package/dist/space-events/index.d.ts.map +1 -0
- package/dist/space-info/decrypt-space-info.d.ts.map +1 -0
- package/dist/space-info/encrypt-and-sign-space-info.d.ts.map +1 -0
- package/dist/space-info/index.d.ts.map +1 -0
- package/dist/store-connect.d.ts.map +1 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/type/type.d.ts.map +1 -0
- package/dist/utils/automergeId.d.ts +9 -0
- package/dist/utils/automergeId.d.ts.map +1 -0
- package/dist/utils/automergeId.js +17 -0
- package/dist/utils/automergeId.js.map +1 -0
- package/dist/utils/generateId.d.ts +15 -0
- package/dist/utils/generateId.d.ts.map +1 -0
- package/dist/utils/generateId.js +18 -0
- package/dist/utils/generateId.js.map +1 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/package.json +35 -0
- package/src/connect/auth-storage.ts +67 -0
- package/src/connect/create-app-identity.ts +16 -0
- package/src/connect/create-auth-url.ts +42 -0
- package/src/connect/create-callback-params.ts +30 -0
- package/src/connect/create-identity-keys.ts +20 -0
- package/src/connect/identity-encryption.ts +232 -0
- package/src/connect/index.ts +10 -0
- package/src/connect/login.ts +114 -0
- package/src/connect/parse-auth-params.ts +37 -0
- package/src/connect/parse-callback-params.ts +67 -0
- package/src/connect/prove-ownership.ts +58 -0
- package/src/connect/types.ts +67 -0
- package/src/entity/create.ts +58 -0
- package/src/entity/decodedEntitiesCache.ts +38 -0
- package/src/entity/delete.ts +52 -0
- package/src/entity/entity.ts +26 -0
- package/src/entity/entityRelationParentsMap.ts +6 -0
- package/src/entity/findMany.ts +506 -0
- package/src/entity/findOne.ts +34 -0
- package/src/entity/getEntityRelations.ts +45 -0
- package/src/entity/hasValidTypesProperty.ts +8 -0
- package/src/entity/index.ts +8 -0
- package/src/entity/relationParentsMap.ts +6 -0
- package/src/entity/removeRelation.ts +21 -0
- package/src/entity/test.ts +0 -0
- package/src/entity/types.ts +100 -0
- package/src/entity/update.ts +58 -0
- package/src/entity/variant-schema.ts +677 -0
- package/src/identity/auth-storage.ts +57 -0
- package/src/identity/get-verified-identity.ts +53 -0
- package/src/identity/identity-encryption.ts +140 -0
- package/src/identity/index.ts +6 -0
- package/src/identity/logout.ts +8 -0
- package/src/identity/prove-ownership.ts +58 -0
- package/src/identity/types.ts +44 -0
- package/src/inboxes/create-inbox.ts +102 -0
- package/src/inboxes/get-list-inboxes.ts +52 -0
- package/src/inboxes/index.ts +10 -0
- package/src/inboxes/merge-messages.ts +28 -0
- package/src/inboxes/message-encryption.ts +35 -0
- package/src/inboxes/message-validation.ts +66 -0
- package/src/inboxes/prepare-message.ts +85 -0
- package/src/inboxes/recover-inbox-creator.ts +29 -0
- package/src/inboxes/recover-inbox-message-signer.ts +42 -0
- package/src/inboxes/send-message.ts +75 -0
- package/src/inboxes/types.ts +9 -0
- package/src/index.ts +13 -0
- package/src/key/create-key.ts +27 -0
- package/src/key/decrypt-key.ts +19 -0
- package/src/key/encrypt-key.ts +27 -0
- package/src/key/index.ts +4 -0
- package/src/key/key-box.ts +31 -0
- package/src/messages/decrypt-message.ts +13 -0
- package/src/messages/encrypt-message.ts +14 -0
- package/src/messages/index.ts +5 -0
- package/src/messages/serialize.ts +24 -0
- package/src/messages/signed-update-message.ts +84 -0
- package/src/messages/types.ts +506 -0
- package/src/space-events/accept-invitation.ts +36 -0
- package/src/space-events/apply-event.ts +150 -0
- package/src/space-events/create-inbox.ts +56 -0
- package/src/space-events/create-invitation.ts +41 -0
- package/src/space-events/create-space.ts +35 -0
- package/src/space-events/delete-space.ts +36 -0
- package/src/space-events/hash-event.ts +10 -0
- package/src/space-events/index.ts +8 -0
- package/src/space-events/types.ts +137 -0
- package/src/space-info/decrypt-space-info.ts +22 -0
- package/src/space-info/encrypt-and-sign-space-info.ts +50 -0
- package/src/space-info/index.ts +3 -0
- package/src/space-info/types.ts +7 -0
- package/src/store-connect.ts +504 -0
- package/src/store.ts +493 -0
- package/src/type/type.ts +25 -0
- package/src/types.ts +47 -0
- package/src/utils/assertExhaustive.ts +3 -0
- package/src/utils/automergeId.ts +18 -0
- package/src/utils/base58.ts +74 -0
- package/src/utils/generateId.ts +18 -0
- package/src/utils/hexBytesAddressUtils.ts +25 -0
- package/src/utils/index.ts +8 -0
- package/src/utils/internal/base58Utils.ts +47 -0
- package/src/utils/internal/deep-merge.ts +38 -0
- package/src/utils/isRelationField.ts +9 -0
- package/src/utils/jsc.ts +94 -0
- 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,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
|
+
};
|