@rljson/bs 0.0.18

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 (42) hide show
  1. package/LICENSE +21 -0
  2. package/README.architecture.md +9 -0
  3. package/README.blog.md +11 -0
  4. package/README.contributors.md +32 -0
  5. package/README.md +24 -0
  6. package/README.public.md +15 -0
  7. package/README.trouble.md +23 -0
  8. package/dist/README.architecture.md +9 -0
  9. package/dist/README.blog.md +11 -0
  10. package/dist/README.contributors.md +32 -0
  11. package/dist/README.md +24 -0
  12. package/dist/README.public.md +15 -0
  13. package/dist/README.trouble.md +23 -0
  14. package/dist/bs-mem.d.ts +33 -0
  15. package/dist/bs-test-setup.d.ts +25 -0
  16. package/dist/bs.d.ts +108 -0
  17. package/dist/bs.js +129 -0
  18. package/dist/conformance-tests/bs-conformance.setup.ts +39 -0
  19. package/dist/conformance-tests/bs-conformance.spec.ts +528 -0
  20. package/dist/conformance-tests/goldens/binary-content.json +4 -0
  21. package/dist/conformance-tests/goldens/consistent-ordering.json +14 -0
  22. package/dist/conformance-tests/goldens/deduplication.json +4 -0
  23. package/dist/conformance-tests/goldens/different-content.json +10 -0
  24. package/dist/conformance-tests/goldens/empty-content.json +4 -0
  25. package/dist/conformance-tests/goldens/from-buffer.json +4 -0
  26. package/dist/conformance-tests/goldens/from-stream.json +4 -0
  27. package/dist/conformance-tests/goldens/from-string.json +4 -0
  28. package/dist/conformance-tests/goldens/get-blob-properties.json +4 -0
  29. package/dist/conformance-tests/goldens/get-blob-stream.json +1 -0
  30. package/dist/conformance-tests/goldens/large-content.json +4 -0
  31. package/dist/conformance-tests/goldens/list-all-blobs.json +14 -0
  32. package/dist/conformance-tests/goldens/list-by-prefix.json +6 -0
  33. package/dist/conformance-tests/goldens/paginate-all-results.json +7 -0
  34. package/dist/conformance-tests/goldens/paginated-listing.json +26 -0
  35. package/dist/conformance-tests/goldens/range-request.json +6 -0
  36. package/dist/conformance-tests/goldens/retrieve-binary.json +6 -0
  37. package/dist/conformance-tests/goldens/retrieve-blob.json +6 -0
  38. package/dist/conformance-tests/scripts/install-conformance-tests.js +103 -0
  39. package/dist/example.d.ts +1 -0
  40. package/dist/index.d.ts +3 -0
  41. package/dist/src/example.ts +9 -0
  42. package/package.json +62 -0
@@ -0,0 +1,528 @@
1
+ // @license
2
+ // Copyright (c) 2026 Rljson
3
+ //
4
+ // Use of this source code is governed by terms that can be
5
+ // found in the LICENSE file in the root of this package.
6
+
7
+ // ⚠️ DO NOT MODIFY THIS FILE DIRECTLY ⚠️
8
+ //
9
+ // This file is a copy of @rljson/bs/test/bs-conformance.spec.ts.
10
+ //
11
+ // To make changes, please execute the following steps:
12
+ // 1. Clone <https://github.com/rljson/bs>
13
+ // 2. Make changes to the original file in the test folder
14
+ // 3. Submit a pull request
15
+ // 4. Publish a the new changes to npm
16
+
17
+
18
+ import {
19
+ afterAll,
20
+ afterEach,
21
+ beforeAll,
22
+ beforeEach,
23
+ describe,
24
+ expect,
25
+ it,
26
+ } from 'vitest';
27
+
28
+ import { Bs, BsTestSetup } from '@rljson/bs';
29
+
30
+ import { testSetup } from './bs-conformance.setup.ts';
31
+ import { expectGolden, ExpectGoldenOptions } from './setup/goldens.ts';
32
+
33
+ const ego: ExpectGoldenOptions = {
34
+ npmUpdateGoldensEnabled: false,
35
+ };
36
+
37
+ /**
38
+ * Conformance tests for Bs implementations.
39
+ * Any implementation of the Bs interface should pass these tests.
40
+ *
41
+ * @param externalTestSetup - Optional test setup for external implementations
42
+ */
43
+ export const runBsConformanceTests = (
44
+ externalTestSetup?: () => BsTestSetup,
45
+ ) => {
46
+ return describe('Bs Conformance', () => {
47
+ let bs: Bs;
48
+ let setup: BsTestSetup;
49
+
50
+ beforeAll(async () => {
51
+ setup = externalTestSetup ? externalTestSetup() : testSetup();
52
+ await setup.beforeAll();
53
+ });
54
+
55
+ beforeEach(async () => {
56
+ await setup.beforeEach();
57
+ bs = setup.bs;
58
+ });
59
+
60
+ afterEach(async () => {
61
+ await setup.afterEach();
62
+ });
63
+
64
+ afterAll(async () => {
65
+ await setup.afterAll();
66
+ });
67
+
68
+ describe('setBlob', () => {
69
+ it('should store a blob from Buffer and return properties', async () => {
70
+ const content = Buffer.from('Hello, World!');
71
+ const result = await bs.setBlob(content);
72
+
73
+ expect(result.blobId).toBeDefined();
74
+ expect(result.blobId).toHaveLength(22);
75
+ expect(result.size).toBe(13);
76
+ expect(result.createdAt).toBeInstanceOf(Date);
77
+
78
+ delete (result as any).createdAt;
79
+ await expectGolden('bs-conformance/from-buffer.json', ego).toBe(
80
+ result as any,
81
+ );
82
+ });
83
+
84
+ it('should store a blob from string', async () => {
85
+ const content = 'Test string content';
86
+ const result = await bs.setBlob(content);
87
+
88
+ expect(result.blobId).toBeDefined();
89
+ expect(result.size).toBe(19);
90
+
91
+ delete (result as any).createdAt;
92
+ await expectGolden('bs-conformance/from-string.json', ego).toBe(
93
+ result as any,
94
+ );
95
+ });
96
+
97
+ it('should store a blob from ReadableStream', async () => {
98
+ const content = 'Stream content';
99
+ const stream = new ReadableStream({
100
+ start(controller) {
101
+ controller.enqueue(new TextEncoder().encode(content));
102
+ controller.close();
103
+ },
104
+ });
105
+
106
+ const result = await bs.setBlob(stream);
107
+
108
+ expect(result.blobId).toBeDefined();
109
+ expect(result.size).toBe(14);
110
+
111
+ delete (result as any).createdAt;
112
+ await expectGolden('bs-conformance/from-stream.json', ego).toBe(
113
+ result as any,
114
+ );
115
+ });
116
+
117
+ it('should deduplicate identical content', async () => {
118
+ const content = Buffer.from('Duplicate content');
119
+
120
+ const result1 = await bs.setBlob(content);
121
+ const result2 = await bs.setBlob(content);
122
+
123
+ expect(result1.blobId).toBe(result2.blobId);
124
+ expect(result1.createdAt).toEqual(result2.createdAt);
125
+
126
+ delete (result1 as any).createdAt;
127
+ await expectGolden('bs-conformance/deduplication.json', ego).toBe(
128
+ result1 as any,
129
+ );
130
+ });
131
+
132
+ it('should generate different blob IDs for different content', async () => {
133
+ const content1 = Buffer.from('Content 1');
134
+ const content2 = Buffer.from('Content 2');
135
+
136
+ const result1 = await bs.setBlob(content1);
137
+ const result2 = await bs.setBlob(content2);
138
+
139
+ expect(result1.blobId).not.toBe(result2.blobId);
140
+
141
+ delete (result1 as any).createdAt;
142
+ delete (result2 as any).createdAt;
143
+ await expectGolden('bs-conformance/different-content.json', ego).toBe({
144
+ blob1: result1 as any,
145
+ blob2: result2 as any,
146
+ });
147
+ });
148
+
149
+ it('should handle empty content', async () => {
150
+ const empty = Buffer.from('');
151
+ const result = await bs.setBlob(empty);
152
+
153
+ expect(result.blobId).toBeDefined();
154
+ expect(result.size).toBe(0);
155
+
156
+ delete (result as any).createdAt;
157
+ await expectGolden('bs-conformance/empty-content.json', ego).toBe(
158
+ result as any,
159
+ );
160
+ });
161
+
162
+ it('should handle binary content', async () => {
163
+ const binaryData = Buffer.from([0, 1, 2, 3, 255, 254, 253]);
164
+ const result = await bs.setBlob(binaryData);
165
+
166
+ expect(result.blobId).toBeDefined();
167
+ expect(result.size).toBe(7);
168
+
169
+ delete (result as any).createdAt;
170
+ await expectGolden('bs-conformance/binary-content.json', ego).toBe(
171
+ result as any,
172
+ );
173
+ });
174
+ });
175
+
176
+ describe('getBlob', () => {
177
+ it('should retrieve stored blob', async () => {
178
+ const content = Buffer.from('Retrieve me!');
179
+ const { blobId } = await bs.setBlob(content);
180
+
181
+ const result = await bs.getBlob(blobId);
182
+
183
+ expect(result.content.toString()).toBe('Retrieve me!');
184
+ expect(result.properties.blobId).toBe(blobId);
185
+ expect(result.properties.size).toBe(12);
186
+
187
+ expect(result.properties.createdAt).toBeInstanceOf(Date);
188
+
189
+ delete (result.properties as any).createdAt;
190
+ await expectGolden('bs-conformance/retrieve-blob.json', ego).toBe({
191
+ properties: result.properties as any,
192
+ });
193
+ });
194
+
195
+ it('should throw error for non-existent blob', async () => {
196
+ await expect(bs.getBlob('nonexistent1234567890')).rejects.toThrow(
197
+ 'Blob not found',
198
+ );
199
+ });
200
+
201
+ it('should support range requests', async () => {
202
+ const content = Buffer.from('0123456789');
203
+ const { blobId } = await bs.setBlob(content);
204
+
205
+ const result = await bs.getBlob(blobId, {
206
+ range: { start: 2, end: 5 },
207
+ });
208
+
209
+ expect(result.content.toString()).toBe('234');
210
+
211
+ expect(result.properties.blobId).toBe(blobId);
212
+ expect(result.properties.size).toBe(10); // size is total size
213
+
214
+ expect(result.properties.createdAt).toBeInstanceOf(Date);
215
+
216
+ delete (result.properties as any).createdAt;
217
+ await expectGolden('bs-conformance/range-request.json', ego).toBe({
218
+ properties: result.properties as any,
219
+ });
220
+ });
221
+
222
+ it('should retrieve binary content correctly', async () => {
223
+ const binaryData = Buffer.from([0, 1, 2, 3, 255, 254, 253]);
224
+ const { blobId } = await bs.setBlob(binaryData);
225
+
226
+ const result = await bs.getBlob(blobId);
227
+
228
+ expect(result.content).toEqual(binaryData);
229
+
230
+ expect(result.properties.blobId).toBe(blobId);
231
+ expect(result.properties.size).toBe(7);
232
+
233
+ expect(result.properties.createdAt).toBeInstanceOf(Date);
234
+
235
+ delete (result.properties as any).createdAt;
236
+ await expectGolden('bs-conformance/retrieve-binary.json', ego).toBe({
237
+ properties: result.properties as any,
238
+ });
239
+ });
240
+
241
+ describe('getBlobStream', () => {
242
+ it('should return a ReadableStream for blob', async () => {
243
+ const content = Buffer.from('Stream this content');
244
+ const { blobId } = await bs.setBlob(content);
245
+
246
+ const stream = await bs.getBlobStream(blobId);
247
+
248
+ expect(stream).toBeInstanceOf(ReadableStream);
249
+
250
+ const reader = stream.getReader();
251
+ const chunks: Uint8Array[] = [];
252
+ while (true) {
253
+ const { done, value } = await reader.read();
254
+ if (done) break;
255
+ chunks.push(value);
256
+ }
257
+
258
+ const result = Buffer.concat(chunks).toString();
259
+ expect(result).toBe('Stream this content');
260
+
261
+ delete (result as any).createdAt;
262
+ await expectGolden('bs-conformance/get-blob-stream.json', ego).toBe(
263
+ result as any,
264
+ );
265
+ });
266
+
267
+ it('should throw error for non-existent blob', async () => {
268
+ await expect(
269
+ bs.getBlobStream('nonexistent1234567890'),
270
+ ).rejects.toThrow('Blob not found');
271
+ });
272
+ });
273
+
274
+ describe('deleteBlob', () => {
275
+ it('should delete an existing blob', async () => {
276
+ const content = Buffer.from('Delete me');
277
+ const { blobId } = await bs.setBlob(content);
278
+
279
+ expect(await bs.blobExists(blobId)).toBe(true);
280
+
281
+ await bs.deleteBlob(blobId);
282
+
283
+ expect(await bs.blobExists(blobId)).toBe(false);
284
+ });
285
+
286
+ it('should throw error when deleting non-existent blob', async () => {
287
+ await expect(bs.deleteBlob('nonexistent1234567890')).rejects.toThrow(
288
+ 'Blob not found',
289
+ );
290
+ });
291
+ });
292
+
293
+ describe('blobExists', () => {
294
+ it('should return true for existing blob', async () => {
295
+ const content = Buffer.from('I exist');
296
+ const { blobId } = await bs.setBlob(content);
297
+
298
+ expect(await bs.blobExists(blobId)).toBe(true);
299
+ });
300
+
301
+ it('should return false for non-existent blob', async () => {
302
+ expect(await bs.blobExists('nonexistent1234567890')).toBe(false);
303
+ });
304
+ });
305
+
306
+ describe('getBlobProperties', () => {
307
+ it('should return properties for existing blob', async () => {
308
+ const content = Buffer.from('Properties test');
309
+ const { blobId, size, createdAt } = await bs.setBlob(content);
310
+
311
+ const properties = await bs.getBlobProperties(blobId);
312
+
313
+ expect(properties.blobId).toBe(blobId);
314
+ expect(properties.size).toBe(size);
315
+ expect(properties.createdAt).toEqual(createdAt);
316
+
317
+ delete (properties as any).createdAt;
318
+ await expectGolden(
319
+ 'bs-conformance/get-blob-properties.json',
320
+ ego,
321
+ ).toBe(properties as any);
322
+ });
323
+
324
+ it('should throw error for non-existent blob', async () => {
325
+ await expect(
326
+ bs.getBlobProperties('nonexistent1234567890'),
327
+ ).rejects.toThrow('Blob not found');
328
+ });
329
+ });
330
+
331
+ describe('listBlobs', () => {
332
+ it('should return empty list when no blobs', async () => {
333
+ const result = await bs.listBlobs();
334
+
335
+ expect(result.blobs).toEqual([]);
336
+ expect(result.continuationToken).toBeUndefined();
337
+ });
338
+
339
+ it('should list all blobs', async () => {
340
+ await bs.setBlob('Blob 1');
341
+ await bs.setBlob('Blob 2');
342
+ await bs.setBlob('Blob 3');
343
+
344
+ const result = await bs.listBlobs();
345
+
346
+ expect(result.blobs).toHaveLength(3);
347
+ expect(result.continuationToken).toBeUndefined();
348
+
349
+ await expectGolden('bs-conformance/list-all-blobs.json', ego).toBe(
350
+ result.blobs.map((b) => ({
351
+ blobId: b.blobId,
352
+ size: b.size,
353
+ })) as any,
354
+ );
355
+ });
356
+
357
+ it('should filter by prefix', async () => {
358
+ const blob1 = await bs.setBlob('Blob 1');
359
+ await bs.setBlob('Blob 2');
360
+ await bs.setBlob('Blob 3');
361
+
362
+ const prefix = blob1.blobId.substring(0, 2);
363
+
364
+ const result = await bs.listBlobs({ prefix });
365
+
366
+ expect(result.blobs.length).toBeGreaterThanOrEqual(1);
367
+ expect(result.blobs.every((b) => b.blobId.startsWith(prefix))).toBe(
368
+ true,
369
+ );
370
+
371
+ await expectGolden('bs-conformance/list-by-prefix.json', ego).toBe(
372
+ result.blobs.map((b) => ({
373
+ blobId: b.blobId,
374
+ size: b.size,
375
+ })) as any,
376
+ );
377
+ });
378
+
379
+ it('should support pagination with maxResults', async () => {
380
+ await bs.setBlob('Blob 1');
381
+ await bs.setBlob('Blob 2');
382
+ await bs.setBlob('Blob 3');
383
+ await bs.setBlob('Blob 4');
384
+
385
+ const page1 = await bs.listBlobs({ maxResults: 2 });
386
+
387
+ expect(page1.blobs).toHaveLength(2);
388
+ expect(page1.continuationToken).toBeDefined();
389
+
390
+ const page2 = await bs.listBlobs({
391
+ maxResults: 2,
392
+ continuationToken: page1.continuationToken,
393
+ });
394
+
395
+ expect(page2.blobs).toHaveLength(2);
396
+ expect(page2.continuationToken).toBeUndefined();
397
+
398
+ const allBlobs = [...page1.blobs, ...page2.blobs];
399
+ expect(allBlobs).toHaveLength(4);
400
+
401
+ await expectGolden('bs-conformance/paginated-listing.json', ego).toBe(
402
+ {
403
+ page1: {
404
+ blobs: page1.blobs.map((b) => ({
405
+ blobId: b.blobId,
406
+ size: b.size,
407
+ })),
408
+ },
409
+ page2: {
410
+ blobs: page2.blobs.map((b) => ({
411
+ blobId: b.blobId,
412
+ size: b.size,
413
+ })),
414
+ },
415
+ } as any,
416
+ );
417
+ });
418
+
419
+ it('should paginate through all results', async () => {
420
+ await bs.setBlob('Blob 1');
421
+ await bs.setBlob('Blob 2');
422
+ await bs.setBlob('Blob 3');
423
+ await bs.setBlob('Blob 4');
424
+ await bs.setBlob('Blob 5');
425
+
426
+ const allBlobs: string[] = [];
427
+ let continuationToken: string | undefined;
428
+
429
+ do {
430
+ const page = await bs.listBlobs({
431
+ maxResults: 2,
432
+ continuationToken,
433
+ });
434
+ allBlobs.push(...page.blobs.map((b) => b.blobId));
435
+ continuationToken = page.continuationToken;
436
+ } while (continuationToken);
437
+
438
+ expect(allBlobs).toHaveLength(5);
439
+ expect(new Set(allBlobs).size).toBe(5);
440
+
441
+ await expectGolden(
442
+ 'bs-conformance/paginate-all-results.json',
443
+ ego,
444
+ ).toBe(allBlobs as any);
445
+ });
446
+
447
+ it('should return blobs in consistent order', async () => {
448
+ await bs.setBlob('C');
449
+ await bs.setBlob('A');
450
+ await bs.setBlob('B');
451
+
452
+ const result1 = await bs.listBlobs();
453
+ const result2 = await bs.listBlobs();
454
+
455
+ expect(result1.blobs.map((b) => b.blobId)).toEqual(
456
+ result2.blobs.map((b) => b.blobId),
457
+ );
458
+
459
+ await expectGolden(
460
+ 'bs-conformance/consistent-ordering.json',
461
+ ego,
462
+ ).toBe(
463
+ result1.blobs.map((b) => ({
464
+ blobId: b.blobId,
465
+ size: b.size,
466
+ })) as any,
467
+ );
468
+ });
469
+ });
470
+
471
+ describe('generateSignedUrl', () => {
472
+ it('should generate a signed URL for existing blob', async () => {
473
+ const content = Buffer.from('Sign me');
474
+ const { blobId } = await bs.setBlob(content);
475
+
476
+ const url = await bs.generateSignedUrl(blobId, 3600);
477
+
478
+ expect(url).toContain(blobId);
479
+ expect(url).toContain('expires=');
480
+ expect(url).toContain('permissions=read');
481
+ });
482
+
483
+ it('should support different permissions', async () => {
484
+ const content = Buffer.from('Delete permission');
485
+ const { blobId } = await bs.setBlob(content);
486
+
487
+ const url = await bs.generateSignedUrl(blobId, 3600, 'delete');
488
+
489
+ expect(url).toContain('permissions=delete');
490
+ });
491
+
492
+ it('should throw error for non-existent blob', async () => {
493
+ await expect(
494
+ bs.generateSignedUrl('nonexistent1234567890', 3600),
495
+ ).rejects.toThrow('Blob not found');
496
+ });
497
+ });
498
+
499
+ describe('content-addressable behavior', () => {
500
+ it('should generate same blob ID for same content', async () => {
501
+ const content = 'Identical content';
502
+
503
+ const result1 = await bs.setBlob(content);
504
+ const result2 = await bs.setBlob(content);
505
+
506
+ expect(result1.blobId).toBe(result2.blobId);
507
+ });
508
+
509
+ it('should handle large content', async () => {
510
+ const largeContent = Buffer.alloc(1024 * 1024, 'x'); // 1MB
511
+ const result = await bs.setBlob(largeContent);
512
+
513
+ expect(result.size).toBe(1024 * 1024);
514
+
515
+ const retrieved = await bs.getBlob(result.blobId);
516
+ expect(retrieved.content.length).toBe(1024 * 1024);
517
+
518
+ delete (result as any).createdAt;
519
+ await expectGolden('bs-conformance/large-content.json', ego).toBe(
520
+ result as any,
521
+ );
522
+ });
523
+ });
524
+ });
525
+ });
526
+ };
527
+ // Run conformance tests for BsMem
528
+ runBsConformanceTests();
@@ -0,0 +1,4 @@
1
+ {
2
+ "blobId": "24mCTTmjD0i1x5d11fAfSF",
3
+ "size": 7
4
+ }
@@ -0,0 +1,14 @@
1
+ [
2
+ {
3
+ "blobId": "335w5QIVRPSDS77mSp43if",
4
+ "size": 1
5
+ },
6
+ {
7
+ "blobId": "ayPA1fNdGxH5toPwsKYXNV",
8
+ "size": 1
9
+ },
10
+ {
11
+ "blobId": "VZrq0IJk1XldOQlxjN0Fq9",
12
+ "size": 1
13
+ }
14
+ ]
@@ -0,0 +1,4 @@
1
+ {
2
+ "blobId": "BioWqSKd8OsOArN-HeaeVt",
3
+ "size": 17
4
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "blob1": {
3
+ "blobId": "zVbQZAnSadvBrORwCB8dMu",
4
+ "size": 9
5
+ },
6
+ "blob2": {
7
+ "blobId": "G4cHUz02KkjQPJIGVMtax1",
8
+ "size": 9
9
+ }
10
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "blobId": "47DEQpj8HBSa-_TImW-5JC",
3
+ "size": 0
4
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "blobId": "3_1gIbsr1bCvZ2KQgJ7DpT",
3
+ "size": 13
4
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "blobId": "OzYVdUU9641aGUFlvTLODU",
3
+ "size": 14
4
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "blobId": "1_NpRP2487MVYnI126TIIC",
3
+ "size": 19
4
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "blobId": "xS2ahBuoP9Nmj7JGZNzd_x",
3
+ "size": 15
4
+ }
@@ -0,0 +1 @@
1
+ "Stream this content"
@@ -0,0 +1,4 @@
1
+ {
2
+ "blobId": "j5kLoLV3tRzwCeoEk2jBa7",
3
+ "size": 1048576
4
+ }
@@ -0,0 +1,14 @@
1
+ [
2
+ {
3
+ "blobId": "CLJcu3tHpMGYkTh2v22Btk",
4
+ "size": 6
5
+ },
6
+ {
7
+ "blobId": "mxtLCMWK9yZk85Ndu3soJW",
8
+ "size": 6
9
+ },
10
+ {
11
+ "blobId": "wgDsFA1U0THzo3usEvi4k5",
12
+ "size": 6
13
+ }
14
+ ]
@@ -0,0 +1,6 @@
1
+ [
2
+ {
3
+ "blobId": "mxtLCMWK9yZk85Ndu3soJW",
4
+ "size": 6
5
+ }
6
+ ]
@@ -0,0 +1,7 @@
1
+ [
2
+ "565YlyyBTZnIGRzkosTXgC",
3
+ "CLJcu3tHpMGYkTh2v22Btk",
4
+ "mxtLCMWK9yZk85Ndu3soJW",
5
+ "wgDsFA1U0THzo3usEvi4k5",
6
+ "wUtA3ddBVABbzU7s-K9mN1"
7
+ ]
@@ -0,0 +1,26 @@
1
+ {
2
+ "page1": {
3
+ "blobs": [
4
+ {
5
+ "blobId": "CLJcu3tHpMGYkTh2v22Btk",
6
+ "size": 6
7
+ },
8
+ {
9
+ "blobId": "mxtLCMWK9yZk85Ndu3soJW",
10
+ "size": 6
11
+ }
12
+ ]
13
+ },
14
+ "page2": {
15
+ "blobs": [
16
+ {
17
+ "blobId": "wgDsFA1U0THzo3usEvi4k5",
18
+ "size": 6
19
+ },
20
+ {
21
+ "blobId": "wUtA3ddBVABbzU7s-K9mN1",
22
+ "size": 6
23
+ }
24
+ ]
25
+ }
26
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "properties": {
3
+ "blobId": "hNiYd_DUBB77a_kaFvAkjy",
4
+ "size": 10
5
+ }
6
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "properties": {
3
+ "blobId": "24mCTTmjD0i1x5d11fAfSF",
4
+ "size": 7
5
+ }
6
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "properties": {
3
+ "blobId": "YI4fVCGSUsNBRzuMCn4qSd",
4
+ "size": 12
5
+ }
6
+ }