7
7
* for more information.
8
8
*/
9
9
10
- import type {
10
+ import {
11
+ Ack ,
11
12
CreateNodeArgs ,
12
13
Eid ,
14
+ GetNodeSliceArgs ,
13
15
NewNodeResponse ,
14
16
Nid ,
17
+ NodeBatch ,
18
+ NodeBatchRequestBody ,
15
19
NodeCreatedVia ,
20
+ NodePatchRequest ,
21
+ NodeUtil ,
16
22
OriginId ,
17
23
StorageApi ,
18
24
TEdgeJson ,
25
+ TNode ,
19
26
TNodeJson ,
27
+ TNodeSliceIterator ,
20
28
} from 'smuggler-api'
21
29
import { NodeType } from 'smuggler-api'
22
30
import { v4 as uuidv4 } from 'uuid'
23
31
import base32Encode from 'base32-encode'
24
32
25
33
import browser from 'webextension-polyfill'
26
- import { unixtime } from 'armoury'
34
+ import { MimeType , unixtime } from 'armoury'
27
35
import lodash from 'lodash'
28
36
29
37
// TODO[snikitin@outlook .com] Describe that "yek" is "key" in reverse,
@@ -90,7 +98,19 @@ class YekLavStore {
90
98
get ( yek : NidYek ) : Promise < NidLav | undefined >
91
99
get ( yek : OriginToNidYek ) : Promise < OriginToNidLav | undefined >
92
100
get ( yek : NidToEdgeYek ) : Promise < NidToEdgeLav | undefined >
93
- get ( yek : Yek ) : Promise < Lav | undefined > {
101
+ get ( yek : NidYek [ ] ) : Promise < NidLav [ ] >
102
+ get ( yek : Yek | Yek [ ] ) : Promise < Lav | Lav [ ] | undefined > {
103
+ if ( Array . isArray ( yek ) ) {
104
+ const keys : string [ ] = yek . map ( ( value : Yek ) => this . stringify ( value ) )
105
+ const records : Promise < Record < string , any > > = this . store . get ( keys )
106
+ return records . then ( ( records : Record < string , any > ) : Promise < Lav [ ] > => {
107
+ const lavs : Lav [ ] = Object . keys ( records ) . map (
108
+ ( key : string ) => records [ key ] as Lav
109
+ )
110
+ return Promise . resolve ( lavs )
111
+ } )
112
+ }
113
+
94
114
const key = this . stringify ( yek )
95
115
const records : Promise < Record < string , any > > = this . store . get ( key )
96
116
return records . then (
@@ -138,16 +158,9 @@ function generateEid(): Eid {
138
158
139
159
async function createNode (
140
160
store : YekLavStore ,
141
- args : CreateNodeArgs ,
142
- _signal ?: AbortSignal
161
+ args : CreateNodeArgs
143
162
) : Promise < NewNodeResponse > {
144
163
// TODO[snikitin@outlook .com] Below keys must become functional somehow.
145
- // Since they only need to function for search-like actions I can store
146
- // create addition kv-pairs just for them where their value is the key
147
- // (potentially prefixed) and a list of Nid is the value. When a node is added,
148
- // an element gets added into the list and when a node is deleted then an
149
- // element is removed from a list
150
- const origin : OriginId | undefined = args . origin
151
164
const created_via : NodeCreatedVia | undefined = args . created_via
152
165
153
166
// TODO[snikitin@outlook .com] This graph structure has to work somehow
@@ -174,8 +187,10 @@ async function createNode(
174
187
} ,
175
188
]
176
189
177
- if ( origin ) {
178
- const yek : OriginToNidYek = { yek : { kind : 'origin->nid' , key : origin } }
190
+ if ( args . origin ) {
191
+ const yek : OriginToNidYek = {
192
+ yek : { kind : 'origin->nid' , key : args . origin } ,
193
+ }
179
194
let lav : OriginToNidLav | undefined = await store . get ( yek )
180
195
lav = lav ?? { lav : { kind : 'origin->nid' , value : [ ] } }
181
196
const nidsWithThisOrigin : Nid [ ] = lav . lav . value
@@ -224,30 +239,103 @@ async function createNode(
224
239
return { nid : node . nid }
225
240
}
226
241
242
+ async function getNode ( {
243
+ store,
244
+ nid,
245
+ } : {
246
+ store : YekLavStore
247
+ nid : Nid
248
+ } ) : Promise < TNode > {
249
+ const yek : NidYek = { yek : { kind : 'nid' , key : nid } }
250
+ const lav : NidLav | undefined = await store . get ( yek )
251
+ if ( lav == null ) {
252
+ throw new Error ( `Failed to get node ${ nid } because it wasn't found` )
253
+ }
254
+ const value : TNodeJson = lav . lav . value
255
+ return NodeUtil . fromJson ( value )
256
+ }
257
+
258
+ async function getNodeBatch (
259
+ store : YekLavStore ,
260
+ req : NodeBatchRequestBody
261
+ ) : Promise < NodeBatch > {
262
+ const yeks : NidYek [ ] = req . nids . map ( ( nid : Nid ) : NidYek => {
263
+ return { yek : { kind : 'nid' , key : nid } }
264
+ } )
265
+ const lavs : NidLav [ ] = await store . get ( yeks )
266
+ return { nodes : lavs . map ( ( lav : NidLav ) => NodeUtil . fromJson ( lav . lav . value ) ) }
267
+ }
268
+
269
+ async function updateNode (
270
+ store : YekLavStore ,
271
+ args : { nid : Nid } & NodePatchRequest
272
+ ) : Promise < Ack > {
273
+ const yek : NidYek = { yek : { kind : 'nid' , key : args . nid } }
274
+ const lav : NidLav | undefined = await store . get ( yek )
275
+ if ( lav == null ) {
276
+ throw new Error ( `Failed to update node ${ args . nid } because it wasn't found` )
277
+ }
278
+ const value : TNodeJson = lav . lav . value
279
+ value . text = args . text != null ? args . text : value . text
280
+ value . index_text =
281
+ args . index_text != null ? args . index_text : value . index_text
282
+ if ( ! args . preserve_update_time ) {
283
+ value . updated_at = unixtime . now ( )
284
+ }
285
+ await store . set ( [ { yek, lav } ] )
286
+ return { ack : true }
287
+ }
288
+
227
289
export function makeLocalStorageApi (
228
- store : browser . Storage . StorageArea
290
+ browserStore : browser . Storage . StorageArea
229
291
) : StorageApi {
292
+ const store = new YekLavStore ( browserStore )
293
+
294
+ const throwUnimplementedError = ( endpoint : string ) => {
295
+ return ( ..._ : any [ ] ) : never => {
296
+ throw new Error (
297
+ `Attempted to call an ${ endpoint } endpoint of local StorageApi which hasn't been implemented yet`
298
+ )
299
+ }
300
+ }
301
+
230
302
return {
231
303
node : {
232
- get : createNode ,
233
- // update: updateNode,
234
- // create: createNode,
235
- // slice: _getNodesSliceIter,
236
- // delete: deleteNode,
237
- // bulkDelete: bulkDeleteNodes,
238
- // batch: {
239
- // get: getNodeBatch,
240
- // },
241
- // url: makeDirectUrl,
304
+ get : ( { nid } : { nid : string ; signal ?: AbortSignal } ) =>
305
+ getNode ( { store, nid } ) ,
306
+ update : (
307
+ args : { nid : string } & NodePatchRequest ,
308
+ _signal ?: AbortSignal
309
+ ) => updateNode ( store , args ) ,
310
+ create : ( args : CreateNodeArgs , _signal ?: AbortSignal ) =>
311
+ createNode ( store , args ) ,
312
+ // TODO[snikitin@outlook .com] Local-hosted slicing implementation is a
313
+ // problem because the datacenter-hosted version depends entirely on
314
+ // time range search which is easy with in SQL, but with a KV-store
315
+ // requires to load all nodes from memory on every "iteration"
316
+ slice : throwUnimplementedError ( 'node.slice' ) ,
317
+ delete : throwUnimplementedError ( 'node.delete' ) ,
318
+ bulkDelete : throwUnimplementedError ( 'node.bulkdDelete' ) ,
319
+ batch : {
320
+ get : ( req : NodeBatchRequestBody , _signal ?: AbortSignal ) =>
321
+ getNodeBatch ( store , req ) ,
322
+ } ,
323
+ url : throwUnimplementedError ( 'node.url' ) ,
242
324
} ,
325
+ // TODO[snikitin@outlook .com] At the time of this writing blob.upload and
326
+ // blob_index.build are used together to create a single searchable blob node.
327
+ // blob.upload is easy to implement for local storage while blob_index.build
328
+ // is more difficult. Given how important search is for Mazed goals at the
329
+ // time of writing, it makes little sense to implement any of them until
330
+ // blob_index.build is usable.
243
331
blob : {
244
- // upload: uploadFiles ,
245
- // sourceUrl: makeBlobSourceUrl ,
332
+ upload : throwUnimplementedError ( 'blob.upload' ) ,
333
+ sourceUrl : throwUnimplementedError ( 'blob.sourceUrl' ) ,
246
334
} ,
247
335
blob_index : {
248
- // build: buildFilesSearchIndex ,
336
+ build : throwUnimplementedError ( 'blob_index.build' ) ,
249
337
cfg : {
250
- // supportsMime: mimeTypeIsSupportedByBuildIndex ,
338
+ supportsMime : ( _mimeType : MimeType ) => false ,
251
339
} ,
252
340
} ,
253
341
edge : {
0 commit comments