Skip to content
This repository was archived by the owner on Oct 25, 2023. It is now read-only.

Commit 9b65ce3

Browse files
authored
local storage: implement storage of associations (#434)
1 parent 10dbc52 commit 9b65ce3

File tree

1 file changed

+187
-17
lines changed

1 file changed

+187
-17
lines changed

archaeologist/src/storage_api_browser_ext.ts

Lines changed: 187 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ import type {
4949
ResourceAttention,
5050
NodeDeleteArgs,
5151
NodeCreatedVia,
52+
UserExternalAssociationType,
53+
GetUserExternalAssociationsResponse,
54+
OriginHash,
55+
ExternalAssociationEnd,
5256
} from 'smuggler-api'
5357
import {
5458
INodeIterator,
@@ -149,6 +153,28 @@ type ExtPipelineToNidYek = GenericYek<
149153
>
150154
type ExtPipelineToNidLav = GenericLav<'ext-pipe-id->nid', { nid: Nid }[]>
151155

156+
type OriginToExtAssociationYek = GenericYek<'origin->ext-assoc', OriginId>
157+
type OriginToExtAssociationValue = {
158+
/**
159+
* `direction`'s meaning may be unintuitive when it comes to writing/reading code.
160+
* Instead, at the time of this writing the intent was to optimise for
161+
* a different purpose - debugging of 'browser.storage.local' data when it
162+
* gets dumped to log/console.
163+
*
164+
* 'origin->ext-assoc' yek/lav pair looks somewhat like
165+
* {yek:*origin-id-1*}/{lav: *direction*, *origin-id-2*}
166+
* which is expected to be read by a developer as:
167+
* " *origin-id-1* has association *from / to* *origin-id-2*"
168+
*/
169+
direction: 'from' | 'to'
170+
other: OriginId
171+
association: UserExternalAssociationType
172+
}
173+
type OriginToExtAssociationLav = GenericLav<
174+
'origin->ext-assoc',
175+
OriginToExtAssociationValue[]
176+
>
177+
152178
type Yek =
153179
| AllNidsYek
154180
| NidToNodeYek
@@ -157,6 +183,7 @@ type Yek =
157183
| OriginToActivityYek
158184
| ExtPipelineYek
159185
| ExtPipelineToNidYek
186+
| OriginToExtAssociationYek
160187
type Lav =
161188
| AllNidsLav
162189
| NidToNodeLav
@@ -165,12 +192,18 @@ type Lav =
165192
| OriginToActivityLav
166193
| ExtPipelineLav
167194
| ExtPipelineToNidLav
195+
| OriginToExtAssociationLav
168196

169197
type YekLav = { yek: Yek; lav: Lav }
170198

171199
function isOfArrayKind(
172200
lav: Lav
173-
): lav is OriginToNidLav | NidToEdgeLav | AllNidsLav | ExtPipelineToNidLav {
201+
): lav is
202+
| OriginToNidLav
203+
| NidToEdgeLav
204+
| AllNidsLav
205+
| ExtPipelineToNidLav
206+
| OriginToExtAssociationLav {
174207
return Array.isArray(lav.lav.value)
175208
}
176209

@@ -211,21 +244,36 @@ class YekLavStore {
211244
get(yek: AllNidsYek): Promise<AllNidsLav | undefined>
212245
get(yek: NidToNodeYek): Promise<NidToNodeLav | undefined>
213246
get(yek: OriginToNidYek): Promise<OriginToNidLav | undefined>
247+
get(
248+
yek: OriginToNidYek[]
249+
): Promise<{ yek: OriginToNidYek; lav: OriginToNidLav }[]>
214250
get(yek: NidToEdgeYek): Promise<NidToEdgeLav | undefined>
215-
get(yek: NidToNodeYek[]): Promise<NidToNodeLav[]>
251+
get(yek: NidToNodeYek[]): Promise<{ yek: NidToNodeYek; lav: NidToNodeLav }[]>
216252
get(yek: OriginToActivityYek): Promise<OriginToActivityLav | undefined>
217253
get(yek: ExtPipelineYek): Promise<ExtPipelineLav | undefined>
218254
get(yek: ExtPipelineToNidYek): Promise<ExtPipelineToNidLav | undefined>
255+
get(
256+
yek: OriginToExtAssociationYek
257+
): Promise<OriginToExtAssociationLav | undefined>
219258
get(yek: Yek): Promise<Lav | undefined>
220-
get(yek: Yek | Yek[]): Promise<Lav | Lav[] | undefined> {
259+
get(yek: Yek | Yek[]): Promise<Lav | YekLav[] | undefined> {
221260
if (Array.isArray(yek)) {
222-
const keys: string[] = yek.map((value: Yek) => this.stringify(value))
261+
const keyToYek = new Map<string, Yek>()
262+
yek.forEach((singleYek: Yek) =>
263+
keyToYek.set(this.stringify(singleYek), singleYek)
264+
)
265+
const keys: string[] = Array.from(keyToYek.keys())
223266
const records: Promise<Record<string, any>> = this.store.get(keys)
224-
return records.then((records: Record<string, any>): Promise<Lav[]> => {
225-
const lavs: Lav[] = Object.keys(records).map(
226-
(key: string) => records[key] as Lav
227-
)
228-
return Promise.resolve(lavs)
267+
return records.then((records: Record<string, any>): Promise<YekLav[]> => {
268+
const yeklavs: YekLav[] = []
269+
for (const [key, yek] of keyToYek) {
270+
const lav = key in records ? (records[key] as Lav) : undefined
271+
if (lav == null) {
272+
continue
273+
}
274+
yeklavs.push({ yek, lav })
275+
}
276+
return Promise.resolve(yeklavs)
229277
})
230278
}
231279

@@ -408,6 +456,8 @@ class YekLavStore {
408456
return 'ext-pipe:' + yek.yek.key.pipeline_key
409457
case 'ext-pipe-id->nid':
410458
return 'ext-pipe-id->nid:' + yek.yek.key.pipeline_key
459+
case 'origin->ext-assoc':
460+
return 'origin->ext-assoc:' + yek.yek.key.id
411461
}
412462
}
413463
}
@@ -581,7 +631,9 @@ async function getNodesByOrigin(
581631
return { yek: { kind: 'nid->node', key: nid } }
582632
}
583633
)
584-
const nidLavs: NidToNodeLav[] = await store.get(nidYeks)
634+
const nidLavs: NidToNodeLav[] = (await store.get(nidYeks)).map(
635+
(yeklav) => yeklav.lav
636+
)
585637
return nidLavs.map((lav: NidToNodeLav) => NodeUtil.fromJson(lav.lav.value))
586638
}
587639

@@ -592,7 +644,9 @@ async function getNodeBatch(
592644
const yeks: NidToNodeYek[] = args.nids.map((nid: Nid): NidToNodeYek => {
593645
return { yek: { kind: 'nid->node', key: nid } }
594646
})
595-
const lavs: NidToNodeLav[] = await store.get(yeks)
647+
const lavs: NidToNodeLav[] = (await store.get(yeks)).map(
648+
(yeklav) => yeklav.lav
649+
)
596650
return {
597651
nodes: lavs.map((lav: NidToNodeLav) => NodeUtil.fromJson(lav.lav.value)),
598652
}
@@ -930,7 +984,7 @@ async function advanceUserIngestionProgress(
930984
return { ack: true }
931985
}
932986

933-
export async function getAllNids(
987+
async function getAllNids(
934988
store: YekLavStore,
935989
_args: NodeGetAllNidsArgs
936990
): Promise<Nid[]> {
@@ -946,6 +1000,124 @@ export async function getAllNids(
9461000
return nids
9471001
}
9481002

1003+
async function recordAssociation(
1004+
store: YekLavStore,
1005+
args: ActivityAssociationRecordArgs
1006+
): Promise<Ack> {
1007+
const { from, to } = args.origin
1008+
const yek: OriginToExtAssociationYek = {
1009+
yek: { kind: 'origin->ext-assoc', key: from },
1010+
}
1011+
const reverseYek: OriginToExtAssociationYek = {
1012+
yek: { kind: 'origin->ext-assoc', key: to },
1013+
}
1014+
const orEmpty = (
1015+
input: OriginToExtAssociationLav | undefined
1016+
): OriginToExtAssociationLav => {
1017+
return input != null
1018+
? input
1019+
: { lav: { kind: 'origin->ext-assoc', value: [] } }
1020+
}
1021+
const [lav, rlav]: OriginToExtAssociationLav[] = await Promise.all([
1022+
store.get(yek).then(orEmpty),
1023+
store.get(reverseYek).then(orEmpty),
1024+
])
1025+
1026+
if (!lav.lav.value.every((assoc) => assoc.other.id !== to.id)) {
1027+
throw new Error(`${from} -> ${to} association already exists`)
1028+
}
1029+
if (!rlav.lav.value.every((assoc) => assoc.other.id !== from.id)) {
1030+
throw new Error(
1031+
`${from} -> ${to} association does not exist, but its reverse version does. Data is unexpectedly inconsistent.`
1032+
)
1033+
}
1034+
1035+
lav.lav.value.push({
1036+
direction: 'to',
1037+
other: to,
1038+
association: args.body.association,
1039+
})
1040+
rlav.lav.value.push({
1041+
direction: 'from',
1042+
other: from,
1043+
association: args.body.association,
1044+
})
1045+
await store.set([
1046+
{ yek, lav },
1047+
{ yek: reverseYek, lav: rlav },
1048+
])
1049+
return { ack: true }
1050+
}
1051+
1052+
async function getAssociations(
1053+
store: YekLavStore,
1054+
args: ActivityAssociationGetArgs
1055+
): Promise<GetUserExternalAssociationsResponse> {
1056+
let value: OriginToExtAssociationValue[] = []
1057+
// Step 1: get all association records
1058+
{
1059+
const yek: OriginToExtAssociationYek = {
1060+
yek: { kind: 'origin->ext-assoc', key: args.origin },
1061+
}
1062+
const lav: OriginToExtAssociationLav | undefined = await store.get(yek)
1063+
value = lav?.lav.value ?? []
1064+
}
1065+
if (value.length === 0) {
1066+
return { from: [], to: [] }
1067+
}
1068+
const originToNids = new Map<OriginHash, Nid[]>()
1069+
// Step 2: get nids of all origins found in association records
1070+
{
1071+
const yeks: OriginToNidYek[] = value.map(
1072+
(v: OriginToExtAssociationValue) => {
1073+
return {
1074+
yek: { kind: 'origin->nid', key: v.other },
1075+
}
1076+
}
1077+
)
1078+
yeks.push({ yek: { kind: 'origin->nid', key: args.origin } })
1079+
const yeklavs: {
1080+
yek: OriginToNidYek
1081+
lav: OriginToNidLav
1082+
}[] = await store.get(yeks)
1083+
for (const { yek, lav } of yeklavs) {
1084+
originToNids.set(
1085+
yek.yek.key.id,
1086+
lav.lav.value.map(({ nid }) => nid)
1087+
)
1088+
}
1089+
}
1090+
const ret: GetUserExternalAssociationsResponse = { from: [], to: [] }
1091+
const makeAssociationEnd = (
1092+
origin_hash: OriginHash
1093+
): ExternalAssociationEnd => {
1094+
return { origin_hash, nids: originToNids.get(origin_hash) ?? [] }
1095+
}
1096+
1097+
// Step 3: enrich association records with nids connected to their origins
1098+
value.forEach((association) => {
1099+
switch (association.direction) {
1100+
case 'from': {
1101+
ret.from.push({
1102+
from: makeAssociationEnd(association.other.id),
1103+
to: makeAssociationEnd(args.origin.id),
1104+
association: association.association,
1105+
})
1106+
return
1107+
}
1108+
case 'to': {
1109+
ret.to.push({
1110+
from: makeAssociationEnd(args.origin.id),
1111+
to: makeAssociationEnd(association.other.id),
1112+
association: association.association,
1113+
})
1114+
return
1115+
}
1116+
}
1117+
})
1118+
return ret
1119+
}
1120+
9491121
export function makeBrowserExtStorageApi(
9501122
browserStore: browser.Storage.StorageArea,
9511123
account: UserAccount
@@ -1008,11 +1180,9 @@ export function makeBrowserExtStorageApi(
10081180
getExternalUserActivity(store, args),
10091181
},
10101182
association: {
1011-
// TODO[snikitin@outlook.com] Replace stubs with real implementation
1012-
record: (_args: ActivityAssociationRecordArgs) =>
1013-
Promise.resolve({ ack: true }),
1014-
get: (_args: ActivityAssociationGetArgs) =>
1015-
Promise.resolve({ from: [], to: [] }),
1183+
record: (args: ActivityAssociationRecordArgs) =>
1184+
recordAssociation(store, args),
1185+
get: (args: ActivityAssociationGetArgs) => getAssociations(store, args),
10161186
},
10171187
},
10181188
external: {

0 commit comments

Comments
 (0)