@peerbit/document 13.1.5 → 13.1.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peerbit/document",
3
- "version": "13.1.5",
3
+ "version": "13.1.6",
4
4
  "description": "Document store implementation",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -59,19 +59,19 @@
59
59
  "@multiformats/multiaddr": "^13.0.1",
60
60
  "p-defer": "^4.0.0",
61
61
  "uint8arrays": "^5.1.0",
62
- "@peerbit/indexer-cache": "0.2.8",
63
- "@peerbit/indexer-interface": "3.0.5",
64
62
  "@peerbit/cache": "3.1.0",
65
- "@peerbit/document-interface": "3.2.47",
66
- "@peerbit/indexer-simple": "1.2.9",
67
63
  "@peerbit/crypto": "3.1.2",
68
- "@peerbit/log": "6.2.4",
64
+ "@peerbit/document-interface": "3.2.48",
65
+ "@peerbit/indexer-interface": "3.0.6",
66
+ "@peerbit/indexer-cache": "0.2.9",
67
+ "@peerbit/indexer-simple": "1.2.10",
68
+ "@peerbit/log": "6.2.5",
69
+ "@peerbit/indexer-sqlite3": "3.0.9",
69
70
  "@peerbit/logger": "2.0.1",
70
- "@peerbit/indexer-sqlite3": "3.0.8",
71
71
  "@peerbit/pubsub": "5.3.0",
72
- "@peerbit/program": "6.0.34",
73
- "@peerbit/rpc": "6.1.2",
74
- "@peerbit/shared-log": "13.2.5",
72
+ "@peerbit/rpc": "6.1.3",
73
+ "@peerbit/program": "6.0.35",
74
+ "@peerbit/shared-log": "13.2.6",
75
75
  "@peerbit/stream-interface": "6.0.11"
76
76
  },
77
77
  "optionalDependencies": {
@@ -85,9 +85,9 @@
85
85
  "uuid": "^10.0.0",
86
86
  "@peerbit/log-rust": "1.1.2",
87
87
  "@peerbit/native-backbone": "0.1.4",
88
+ "@peerbit/test-utils": "3.1.6",
88
89
  "@peerbit/time": "3.0.0",
89
- "@peerbit/test-utils": "3.1.5",
90
- "peerbit": "5.3.5"
90
+ "peerbit": "5.3.6"
91
91
  },
92
92
  "repository": {
93
93
  "type": "git",
@@ -102,7 +102,7 @@
102
102
  "clean": "aegir clean",
103
103
  "build": "aegir build --no-bundle",
104
104
  "test": "aegir test --target node",
105
- "test:document-rust-core": "npm run build && PEERBIT_SHARED_LOG_RUST_CORE=1 aegir test -t node --grep '^(?!.*(uses the validated local append path|uses the independent native prepared batch path|uses native shared-log planning for replicated target-none puts|uses commit-only local puts when coordinate persistence is deferred|can delete without being replicator)).*(native conformance guard |operations basic |operations get |operations index |operations search |iterate sort |count approximate |query distribution |returnIndexed |caching |custom index |prefetch )'",
105
+ "test:document-rust-core": "npm run build && PEERBIT_SHARED_LOG_RUST_CORE=1 aegir test -t node --grep '^(?!.*(uses the validated local append path|uses the independent native prepared batch path|uses native shared-log planning for replicated target-none puts|uses commit-only local puts when coordinate persistence is deferred)).*(native conformance guard |operations basic |operations get |operations index |operations search |iterate sort |count approximate |query distribution |returnIndexed |caching |custom index |prefetch )' -- --forbid-pending",
106
106
  "lint": "aegir lint",
107
107
  "test:cov": "aegir test -t node --cov"
108
108
  }
@@ -11,6 +11,11 @@ import { logger as loggerFn } from "@peerbit/logger";
11
11
  const iteratorLogger = loggerFn("peerbit:document:index:iterate");
12
12
 
13
13
  export class ResumableIterators<T extends Record<string, any>> {
14
+ private readonly pendingMarks = new Map<
15
+ string,
16
+ Map<string, indexerTypes.IdKey>
17
+ >();
18
+
14
19
  constructor(
15
20
  readonly index: indexerTypes.Index<T>,
16
21
  readonly queues = new Cache<{
@@ -22,6 +27,36 @@ export class ResumableIterators<T extends Record<string, any>> {
22
27
  // TODO choose upper limit better
23
28
  }
24
29
 
30
+ private markKey(id: indexerTypes.IdKey) {
31
+ return `${id.key instanceof Uint8Array ? "bytes" : typeof id.key}:${String(id.primitive)}`;
32
+ }
33
+
34
+ private retainMarks(id: string, ids: Iterable<indexerTypes.IdKey>) {
35
+ let retained = this.pendingMarks.get(id);
36
+ for (const mark of ids) {
37
+ if (!retained) {
38
+ retained = new Map();
39
+ this.pendingMarks.set(id, retained);
40
+ }
41
+ retained.set(this.markKey(mark), mark);
42
+ }
43
+ }
44
+
45
+ private async applyPendingMarks(
46
+ id: string,
47
+ iterator: indexerTypes.IndexIterator<T, undefined>,
48
+ ) {
49
+ while (true) {
50
+ const retained = this.pendingMarks.get(id);
51
+ if (!retained?.size) {
52
+ this.pendingMarks.delete(id);
53
+ return;
54
+ }
55
+ this.pendingMarks.delete(id);
56
+ await iterator.markYielded?.(retained.values());
57
+ }
58
+ }
59
+
25
60
  async iterateAndFetch(
26
61
  request: SearchRequest | SearchRequestIndexed | IterationRequest,
27
62
  options?: { keepAlive?: boolean },
@@ -31,29 +66,32 @@ export class ResumableIterators<T extends Record<string, any>> {
31
66
  fetch: request.fetch,
32
67
  keepAlive: Boolean(options?.keepAlive),
33
68
  });
34
- const iterator = this.index.iterate(request);
35
- const firstResult = await iterator.next(request.fetch);
36
69
  const keepAlive = options?.keepAlive === true;
37
- if (keepAlive || iterator.done() !== true) {
38
- const cachedIterator = {
39
- iterator,
40
- request,
70
+ const iterator = this.index.iterate(request);
71
+ this.queues.add(request.idString, { iterator, request, keepAlive });
72
+ try {
73
+ await this.applyPendingMarks(request.idString, iterator);
74
+ const firstResult = await iterator.next(request.fetch);
75
+ if (!keepAlive && iterator.done() === true) {
76
+ this.queues.del(request.idString);
77
+ this.pendingMarks.delete(request.idString);
78
+ }
79
+ iteratorLogger("iterate:queued", {
80
+ id: request.idString,
41
81
  keepAlive,
42
- };
43
- this.queues.add(request.idString, cachedIterator);
82
+ done: iterator.done() === true,
83
+ batch: firstResult.length,
84
+ });
85
+ /* console.debug(
86
+ "[ResumableIterators] iterateAndFetch",
87
+ request.idString,
88
+ { keepAlive },
89
+ ); */
90
+ return firstResult;
91
+ } catch (error) {
92
+ this.clear(request.idString);
93
+ throw error;
44
94
  }
45
- iteratorLogger("iterate:queued", {
46
- id: request.idString,
47
- keepAlive,
48
- done: iterator.done() === true,
49
- batch: firstResult.length,
50
- });
51
- /* console.debug(
52
- "[ResumableIterators] iterateAndFetch",
53
- request.idString,
54
- { keepAlive },
55
- ); */
56
- return firstResult;
57
95
  }
58
96
 
59
97
  async next(
@@ -138,6 +176,7 @@ export class ResumableIterators<T extends Record<string, any>> {
138
176
  }
139
177
  }
140
178
  this.queues.del(id);
179
+ this.pendingMarks.delete(id);
141
180
  }
142
181
 
143
182
  async clearAll() {
@@ -155,6 +194,7 @@ export class ResumableIterators<T extends Record<string, any>> {
155
194
  ),
156
195
  );
157
196
  this.queues.clear();
197
+ this.pendingMarks.clear();
158
198
  }
159
199
 
160
200
  has(id: string) {
@@ -179,4 +219,12 @@ export class ResumableIterators<T extends Record<string, any>> {
179
219
  });
180
220
  return pending;
181
221
  }
222
+
223
+ async markYielded(id: string, ids: Iterable<indexerTypes.IdKey>) {
224
+ this.retainMarks(id, ids);
225
+ const iterator = this.queues.get(id);
226
+ if (iterator) {
227
+ await this.applyPendingMarks(id, iterator.iterator);
228
+ }
229
+ }
182
230
  }
package/src/search.ts CHANGED
@@ -24,7 +24,7 @@ import {
24
24
  import { CachedIndex, type QueryCacheOptions } from "@peerbit/indexer-cache";
25
25
  import * as indexerTypes from "@peerbit/indexer-interface";
26
26
  import { HashmapIndex } from "@peerbit/indexer-simple";
27
- import { BORSH_ENCODING, type Encoding, Entry } from "@peerbit/log";
27
+ import { BORSH_ENCODING, type Encoding, type Entry } from "@peerbit/log";
28
28
  import { logger as loggerFn } from "@peerbit/logger";
29
29
  import { ClosedError, Program } from "@peerbit/program";
30
30
  import {
@@ -1179,26 +1179,56 @@ export class DocumentIndex<
1179
1179
  }
1180
1180
  }
1181
1181
 
1182
- /** Head entry safe to borsh-serialize into ResultIndexedValue.entries over the
1183
- * document RPC. Under the native block store, this._log.log.get can return a
1184
- * hollow entry whose payload bytes were never materialized on the JS side;
1185
- * serializing it throws and aborts the whole RPC response. The complete entry is
1186
- * in the block store keyed by hash, so recover it there. No-op for pure JS. */
1187
- private async getSerializableHead(
1188
- hash: string,
1189
- ): Promise<Entry<any> | undefined> {
1190
- const head = await this._log.log.get(hash);
1191
- if (!head?.hash) return head ?? undefined;
1192
- try {
1193
- head.getStorageBytes(); // = serialize(head); throws on a hollow native entry
1194
- return head;
1195
- } catch {
1196
- try {
1197
- return await Entry.fromMultihash(this._log.log.blocks, head.hash);
1198
- } catch {
1199
- return head; // block absent (e.g. pruned) — preserve today's behavior
1182
+ /**
1183
+ * Resolve documents without bypassing the resolver/program caches. Cache and
1184
+ * identity-index hits are collected first; only the remaining heads cross the
1185
+ * log read boundary, and those reads stay aligned in one batch.
1186
+ */
1187
+ private async resolveDocumentsWithBatchedHeads(
1188
+ values: Array<{
1189
+ id?: indexerTypes.IdPrimitive;
1190
+ indexed: I;
1191
+ head: string;
1192
+ }>,
1193
+ ): Promise<{
1194
+ resolved: Array<{ value: T } | undefined>;
1195
+ heads: Array<Entry<Operation> | undefined>;
1196
+ }> {
1197
+ const resolved: Array<{ value: T } | undefined> = new Array(values.length);
1198
+ const heads: Array<Entry<Operation> | undefined> = new Array(values.length);
1199
+ const unresolvedPositions: number[] = [];
1200
+
1201
+ for (let i = 0; i < values.length; i++) {
1202
+ const value = values[i]!;
1203
+ const cached = await this.resolveDocument({
1204
+ ...value,
1205
+ headEntry: null,
1206
+ });
1207
+ if (cached) {
1208
+ resolved[i] = cached;
1209
+ } else {
1210
+ unresolvedPositions.push(i);
1200
1211
  }
1201
1212
  }
1213
+
1214
+ if (unresolvedPositions.length === 0) {
1215
+ return { resolved, heads };
1216
+ }
1217
+
1218
+ const unresolvedHeads = await this._log.log.getMany(
1219
+ unresolvedPositions.map((position) => values[position]!.head),
1220
+ );
1221
+ for (let i = 0; i < unresolvedPositions.length; i++) {
1222
+ const position = unresolvedPositions[i]!;
1223
+ const head = unresolvedHeads[i];
1224
+ heads[position] = head;
1225
+ resolved[position] = await this.resolveDocument({
1226
+ ...values[position]!,
1227
+ headEntry: head ?? null,
1228
+ });
1229
+ }
1230
+
1231
+ return { resolved, heads };
1202
1232
  }
1203
1233
 
1204
1234
  private async wrapPushResults(
@@ -1206,8 +1236,41 @@ export class DocumentIndex<
1206
1236
  resolve: boolean,
1207
1237
  ): Promise<types.Result[]> {
1208
1238
  if (!matches.length) return [];
1239
+ const headsByMatch: Array<Entry<Operation> | undefined> = [];
1240
+ const resolvedByMatch: Array<{ value: T } | undefined> = [];
1241
+ if (!resolve) {
1242
+ headsByMatch.push(
1243
+ ...(await this._log.log.getMany(
1244
+ matches.map((match) => match.__context.head),
1245
+ )),
1246
+ );
1247
+ } else {
1248
+ const indexedPositions: number[] = [];
1249
+ for (let i = 0; i < matches.length; i++) {
1250
+ if (!(matches[i] instanceof this.documentType)) {
1251
+ indexedPositions.push(i);
1252
+ }
1253
+ }
1254
+ const indexedBatch = indexedPositions.length
1255
+ ? await this.resolveDocumentsWithBatchedHeads(
1256
+ indexedPositions.map((position) => {
1257
+ const indexed = matches[position] as WithContext<I>;
1258
+ return {
1259
+ indexed,
1260
+ head: indexed.__context.head,
1261
+ };
1262
+ }),
1263
+ )
1264
+ : { resolved: [], heads: [] };
1265
+ for (let i = 0; i < indexedPositions.length; i++) {
1266
+ const position = indexedPositions[i]!;
1267
+ resolvedByMatch[position] = indexedBatch.resolved[i];
1268
+ headsByMatch[position] = indexedBatch.heads[i];
1269
+ }
1270
+ }
1209
1271
  const results: types.Result[] = [];
1210
- for (const match of matches) {
1272
+ for (let i = 0; i < matches.length; i++) {
1273
+ const match = matches[i]!;
1211
1274
  if (resolve) {
1212
1275
  if (match instanceof this.documentType) {
1213
1276
  const doc = match as WithContext<T>;
@@ -1225,10 +1288,7 @@ export class DocumentIndex<
1225
1288
  }
1226
1289
 
1227
1290
  const indexed = match as WithContext<I>;
1228
- const resolved = await this.resolveDocument({
1229
- indexed,
1230
- head: indexed.__context.head,
1231
- });
1291
+ const resolved = resolvedByMatch[i];
1232
1292
 
1233
1293
  if (resolved) {
1234
1294
  results.push(
@@ -1242,7 +1302,7 @@ export class DocumentIndex<
1242
1302
  continue;
1243
1303
  }
1244
1304
 
1245
- const head = await this.getSerializableHead(indexed.__context.head);
1305
+ const head = headsByMatch[i];
1246
1306
  results.push(
1247
1307
  new types.ResultIndexedValue({
1248
1308
  context: indexed.__context,
@@ -1253,7 +1313,7 @@ export class DocumentIndex<
1253
1313
  );
1254
1314
  } else {
1255
1315
  const indexed = match as WithContext<I>;
1256
- const head = await this.getSerializableHead(indexed.__context.head);
1316
+ const head = headsByMatch[i];
1257
1317
  results.push(
1258
1318
  new types.ResultIndexedValue({
1259
1319
  context: indexed.__context,
@@ -1275,17 +1335,28 @@ export class DocumentIndex<
1275
1335
  return [];
1276
1336
  }
1277
1337
  const drained = queueEntries.splice(0);
1338
+ const resolvedBatch = resolve
1339
+ ? await this.resolveDocumentsWithBatchedHeads(
1340
+ drained.map((entry) => ({
1341
+ indexed: entry.value,
1342
+ head: entry.value.__context.head,
1343
+ })),
1344
+ )
1345
+ : undefined;
1346
+ const heads = resolvedBatch
1347
+ ? resolvedBatch.heads
1348
+ : await this._log.log.getMany(
1349
+ drained.map((entry) => entry.value.__context.head),
1350
+ );
1278
1351
  const results: types.Result[] = [];
1279
- for (const entry of drained) {
1352
+ for (let i = 0; i < drained.length; i++) {
1353
+ const entry = drained[i]!;
1280
1354
  const indexedUnwrapped = Object.assign(
1281
1355
  Object.create(this.indexedType.prototype),
1282
1356
  entry.value,
1283
1357
  );
1284
1358
  if (resolve) {
1285
- const value = await this.resolveDocument({
1286
- indexed: entry.value,
1287
- head: entry.value.__context.head,
1288
- });
1359
+ const value = resolvedBatch!.resolved[i];
1289
1360
  if (value) {
1290
1361
  results.push(
1291
1362
  new types.ResultValue({
@@ -1298,7 +1369,7 @@ export class DocumentIndex<
1298
1369
  continue;
1299
1370
  }
1300
1371
 
1301
- const head = await this.getSerializableHead(entry.value.__context.head);
1372
+ const head = heads[i];
1302
1373
  results.push(
1303
1374
  new types.ResultIndexedValue({
1304
1375
  context: entry.value.__context,
@@ -1308,7 +1379,7 @@ export class DocumentIndex<
1308
1379
  }),
1309
1380
  );
1310
1381
  } else {
1311
- const head = await this.getSerializableHead(entry.value.__context.head);
1382
+ const head = heads[i];
1312
1383
  results.push(
1313
1384
  new types.ResultIndexedValue({
1314
1385
  context: entry.value.__context,
@@ -1358,6 +1429,7 @@ export class DocumentIndex<
1358
1429
  let pendingAdded = added;
1359
1430
  do {
1360
1431
  const batches: types.Result[] = [];
1432
+ let claimedIds: indexerTypes.IdKey[] = [];
1361
1433
  const queued = await this.drainQueuedResults(
1362
1434
  queue.queue,
1363
1435
  resolveFlag,
@@ -1379,6 +1451,21 @@ export class DocumentIndex<
1379
1451
  const wrapped = await this.wrapPushResults(matches, resolveFlag);
1380
1452
  if (wrapped.length) {
1381
1453
  batches.push(...wrapped);
1454
+ claimedIds = wrapped
1455
+ .map((result) =>
1456
+ result instanceof types.ResultValue && result.indexed
1457
+ ? indexerTypes.toId(
1458
+ this.indexByResolver(result.indexed),
1459
+ )
1460
+ : result instanceof types.ResultIndexedValue
1461
+ ? indexerTypes.toId(
1462
+ this.indexByResolver(result.value),
1463
+ )
1464
+ : undefined,
1465
+ )
1466
+ .filter(
1467
+ (id): id is indexerTypes.IdKey => id !== undefined,
1468
+ );
1382
1469
  }
1383
1470
  }
1384
1471
  if (batches.length) {
@@ -1396,6 +1483,12 @@ export class DocumentIndex<
1396
1483
  redundancy: 1,
1397
1484
  }),
1398
1485
  });
1486
+ if (claimedIds.length) {
1487
+ await this._resumableIterators.markYielded(
1488
+ _iteratorId,
1489
+ claimedIds,
1490
+ );
1491
+ }
1399
1492
  }
1400
1493
  pendingAdded = queue.pendingAdded ?? [];
1401
1494
  queue.pendingAdded = undefined;
@@ -3474,6 +3567,7 @@ export class DocumentIndex<
3474
3567
  id?: indexerTypes.IdPrimitive;
3475
3568
  indexed: I;
3476
3569
  head: string;
3570
+ headEntry?: Entry<Operation> | null;
3477
3571
  }): Promise<{ value: T } | undefined> {
3478
3572
  const id =
3479
3573
  value.id ??
@@ -3495,7 +3589,10 @@ export class DocumentIndex<
3495
3589
  return { value: obj as T };
3496
3590
  }
3497
3591
 
3498
- const head = await this._log.log.get(value.head);
3592
+ const head =
3593
+ value.headEntry === undefined
3594
+ ? await this._log.log.get(value.head)
3595
+ : (value.headEntry ?? undefined);
3499
3596
  if (!head) {
3500
3597
  return undefined; // we could end up here if we recently pruned the document and other peers never persisted the entry
3501
3598
  // TODO update changes in index before removing entries from log entry storage
@@ -3633,7 +3730,10 @@ export class DocumentIndex<
3633
3730
  this._resultQueue.set(query.idString, prevQueued);
3634
3731
  }
3635
3732
 
3636
- const filteredResults: types.Result[] = [];
3733
+ const candidates: Array<{
3734
+ result: indexerTypes.IndexedResult<WithContext<I>>;
3735
+ indexed: I;
3736
+ }> = [];
3637
3737
  const resolveDocumentsFlag = resolvesDocuments(fromQuery);
3638
3738
  const replicateIndexFlag = replicatesIndex(fromQuery);
3639
3739
  for (const result of toIterate) {
@@ -3655,11 +3755,29 @@ export class DocumentIndex<
3655
3755
  ) {
3656
3756
  continue;
3657
3757
  }
3758
+ candidates.push({ result, indexed: indexedUnwrapped });
3759
+ }
3760
+
3761
+ const resolvedBatch = resolveDocumentsFlag
3762
+ ? await this.resolveDocumentsWithBatchedHeads(
3763
+ candidates.map(({ result }) => ({
3764
+ indexed: result.value,
3765
+ head: result.value.__context.head,
3766
+ })),
3767
+ )
3768
+ : undefined;
3769
+ const heads = resolvedBatch
3770
+ ? resolvedBatch.heads
3771
+ : candidates.length > 0
3772
+ ? await this._log.log.getMany(
3773
+ candidates.map(({ result }) => result.value.__context.head),
3774
+ )
3775
+ : [];
3776
+ const filteredResults: types.Result[] = [];
3777
+ for (let i = 0; i < candidates.length; i++) {
3778
+ const { result, indexed: indexedUnwrapped } = candidates[i]!;
3658
3779
  if (resolveDocumentsFlag) {
3659
- const value = await this.resolveDocument({
3660
- indexed: result.value,
3661
- head: result.value.__context.head,
3662
- });
3780
+ const value = resolvedBatch!.resolved[i];
3663
3781
 
3664
3782
  if (!value) {
3665
3783
  continue;
@@ -3675,7 +3793,7 @@ export class DocumentIndex<
3675
3793
  );
3676
3794
  } else {
3677
3795
  const context = result.value.__context;
3678
- const head = await this.getSerializableHead(context.head);
3796
+ const head = heads[i];
3679
3797
  if (replicateIndexFlag) {
3680
3798
  if (!head) {
3681
3799
  continue;
@@ -6144,9 +6262,10 @@ export class DocumentIndex<
6144
6262
  continue;
6145
6263
  }
6146
6264
  }
6147
- const id = indexerTypes.toId(
6265
+ const indexId = indexerTypes.toId(
6148
6266
  this.indexByResolver(indexedCandidate),
6149
- ).primitive;
6267
+ );
6268
+ const id = indexId.primitive;
6150
6269
  const existingIndexed = indexedPlaceholders?.get(id);
6151
6270
  if (existingIndexed) {
6152
6271
  if (resolve) {
@@ -6187,6 +6306,10 @@ export class DocumentIndex<
6187
6306
  if (!resolve) {
6188
6307
  ensureIndexedPlaceholders().set(id, placeholder);
6189
6308
  }
6309
+ await this._resumableIterators.markYielded(
6310
+ queryRequestCoerced.idString,
6311
+ [indexId],
6312
+ );
6190
6313
  hasRelevantChange = true;
6191
6314
  }
6192
6315
  }