n3 2.7.12 → 2.9.0

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/src/N3Reasoner.js CHANGED
@@ -23,10 +23,18 @@ export default class N3Reasoner {
23
23
  }
24
24
 
25
25
  _add(subject, predicate, object, graphItem, c, cb) {
26
- // Only add to the remaining indexes if there is not already a value in the index
27
- if (!this._store._addToIndex(graphItem.subjects, subject, predicate, object)) return;
28
- this._store._addToIndex(graphItem.predicates, predicate, object, subject);
29
- this._store._addToIndex(graphItem.objects, object, subject, predicate);
26
+ const store = this._store;
27
+ // Observing views must see each derivation before indexing
28
+ // (stores from other N3 versions have no observers)
29
+ if (store._observers) {
30
+ if (!store._addQuad(subject, predicate, object, this._graphId)) return;
31
+ }
32
+ else {
33
+ // Only add to the remaining indexes if there is not already a value in the index
34
+ if (!store._addToIndex(graphItem.subjects, subject, predicate, object)) return;
35
+ store._addToIndex(graphItem.predicates, predicate, object, subject);
36
+ store._addToIndex(graphItem.objects, object, subject, predicate);
37
+ }
30
38
  // Count genuinely new derivations and fail past the budget. The check comes
31
39
  // after all three indexes are updated, so a caught error leaves the store
32
40
  // in a consistent state (the reasoning result is merely incomplete).
@@ -228,6 +236,8 @@ export default class N3Reasoner {
228
236
  const graphs = this._store._getGraphs();
229
237
  try {
230
238
  for (const graphId in graphs) {
239
+ // Observers compare numeric graph identifiers
240
+ this._graphId = Number(graphId);
231
241
  this._reasonGraphNaive(rules, graphs[graphId]);
232
242
  }
233
243
  }
package/src/N3Store.js CHANGED
@@ -8,6 +8,15 @@ import N3Writer from './N3Writer';
8
8
  const ITERATOR = Symbol('iter');
9
9
  const SIZE = Symbol('size');
10
10
 
11
+ // Neither a registration nor its cleanup record may keep a view or store alive.
12
+ // Initialize lazily so importing N3 and using lazy views needs no weak-reference APIs.
13
+ let observerRegistry;
14
+ function finalizeObserver({ store, observer }) {
15
+ const target = store.deref();
16
+ if (target)
17
+ target._removeObserver(observer);
18
+ }
19
+
11
20
  function hasInIndex(index0, key0, key1, key2) {
12
21
  const index1 = index0 && index0[key0];
13
22
  const index2 = index1 && index1[key1];
@@ -189,12 +198,15 @@ export default class N3Store {
189
198
  this._size = 0;
190
199
  // `_graphs` contains subject, predicate, and object indexes per graph
191
200
  this._graphs = Object.create(null);
201
+ // `_observers` contains weak references to views notified before every mutation
202
+ this._observers = null;
192
203
 
193
204
  // Shift parameters if `quads` is not given
194
205
  if (!options && quads && !quads[0] && !(typeof quads.match === 'function'))
195
206
  options = quads, quads = null;
196
207
  options = options || {};
197
208
  this._factory = options.factory || N3DataFactory;
209
+ this._matchSemantics = validateMatchSemantics(options.matchSemantics);
198
210
  this._entityIndex = options.entityIndex || new N3EntityIndex({ factory: this._factory });
199
211
  this._entities = this._entityIndex._entities;
200
212
  this._termFromId = this._entityIndex._termFromId.bind(this._entityIndex);
@@ -402,6 +414,38 @@ export default class N3Store {
402
414
  return typeof graph !== 'number' ? this._graphs : { [graph]: this._graphs[graph] };
403
415
  }
404
416
 
417
+ // ### `_addObserver` registers a mutation observer.
418
+ _addObserver(view) {
419
+ const observer = new WeakRef(view);
420
+ const registry = observerRegistry || (observerRegistry = new FinalizationRegistry(finalizeObserver));
421
+ registry.register(view, { store: new WeakRef(this), observer }, observer);
422
+ (this._observers || (this._observers = new Set())).add(observer);
423
+ return observer;
424
+ }
425
+
426
+ // ### `_removeObserver` unregisters a mutation observer.
427
+ _removeObserver(observer) {
428
+ observerRegistry.unregister(observer);
429
+ if (this._observers) {
430
+ this._observers.delete(observer);
431
+ if (this._observers.size === 0)
432
+ this._observers = null;
433
+ }
434
+ }
435
+
436
+ // ### `_notifyObservers` notifies observers of a mutation.
437
+ _notifyObservers(subjectId, predicateId, objectId, graphId, added) {
438
+ // Observers can remove themselves during Set iteration
439
+ for (const observer of this._observers) {
440
+ const view = observer.deref();
441
+ if (view)
442
+ view._onParentMutation(subjectId, predicateId, objectId, graphId, added);
443
+ else
444
+ // Finalization may be delayed; also prune collected views during notification.
445
+ this._removeObserver(observer);
446
+ }
447
+ }
448
+
405
449
  // ### `_uniqueEntities` returns a function that accepts an entity ID
406
450
  // and passes the corresponding entity to callback if it hasn't occurred before.
407
451
  _uniqueEntities(callback) {
@@ -435,6 +479,15 @@ export default class N3Store {
435
479
  // Convert terms to internal string representation
436
480
  graph = graph ? this._termToNewNumericId(graph) : 1;
437
481
 
482
+ // Map long IRIs to shared numeric identifiers before updating the indexes.
483
+ subject = this._termToNewNumericId(subject);
484
+ predicate = this._termToNewNumericId(predicate);
485
+ object = this._termToNewNumericId(object);
486
+ return this._addQuad(subject, predicate, object, graph);
487
+ }
488
+
489
+ // ### `_addQuad` adds a quad using identifiers from this store's entity index.
490
+ _addQuad(subject, predicate, object, graph) {
438
491
  // Find the graph that will contain the triple
439
492
  let graphItem = this._graphs[graph];
440
493
  // Create the graph if it doesn't exist yet
@@ -449,12 +502,12 @@ export default class N3Store {
449
502
  Object.freeze(graphItem);
450
503
  }
451
504
 
452
- // Since entities can often be long IRIs, we avoid storing them in every index.
453
- // Instead, we have a separate index that maps entities to numbers,
454
- // which are then used as keys in the other indexes.
455
- subject = this._termToNewNumericId(subject);
456
- predicate = this._termToNewNumericId(predicate);
457
- object = this._termToNewNumericId(object);
505
+ // Notify observers before inserting a new quad so snapshots retain their prior contents
506
+ if (this._observers !== null) {
507
+ if (hasInIndex(graphItem.subjects, subject, predicate, object))
508
+ return false;
509
+ this._notifyObservers(subject, predicate, object, graph, true);
510
+ }
458
511
 
459
512
  if (!this._addToIndex(graphItem.subjects, subject, predicate, object))
460
513
  return false;
@@ -510,17 +563,25 @@ export default class N3Store {
510
563
  // Convert terms to internal string representation
511
564
  graph = graph ? this._termToNumericId(graph) : 1;
512
565
 
513
- // Find internal identifiers for all components
514
- // and verify the quad exists.
566
+ if (!(subject = subject && this._termToNumericId(subject)) ||
567
+ !(predicate = predicate && this._termToNumericId(predicate)) ||
568
+ !(object = object && this._termToNumericId(object)))
569
+ return false;
570
+ return this._removeQuad(subject, predicate, object, graph);
571
+ }
572
+
573
+ // ### `_removeQuad` removes a quad using identifiers from this store's entity index.
574
+ _removeQuad(subject, predicate, object, graph) {
575
+ // Verify the quad exists before notifying observers or changing any indexes.
515
576
  const graphs = this._graphs;
516
- let graphItem, subjects, predicates;
517
- if (!(subject = subject && this._termToNumericId(subject)) || !(predicate = predicate && this._termToNumericId(predicate)) ||
518
- !(object = object && this._termToNumericId(object)) || !(graphItem = graphs[graph]) ||
519
- !(subjects = graphItem.subjects[subject]) ||
520
- !(predicates = subjects[predicate]) ||
521
- !(object in predicates))
577
+ const graphItem = graphs[graph];
578
+ if (!graphItem || !hasInIndex(graphItem.subjects, subject, predicate, object))
522
579
  return false;
523
580
 
581
+ // Notify observers before the mutation
582
+ if (this._observers !== null)
583
+ this._notifyObservers(subject, predicate, object, graph, false);
584
+
524
585
  // Remove it from all indexes
525
586
  this._removeFromIndex(graphItem.subjects, subject, predicate, object);
526
587
  this._removeFromIndex(graphItem.predicates, predicate, object, subject);
@@ -627,8 +688,13 @@ export default class N3Store {
627
688
  // Note: Since a DatasetCore is an unordered set, the order of the quads within the returned sequence is arbitrary.
628
689
  // Setting any field to `undefined` or `null` indicates a wildcard.
629
690
  // For backwards compatibility, the object return also implements the Readable stream interface.
630
- match(subject, predicate, object, graph) {
631
- return new DatasetCoreAndReadableStream(this, subject, predicate, object, graph, { entityIndex: this._entityIndex });
691
+ // `options.matchSemantics` controls how the view reacts to later mutations.
692
+ match(subject, predicate, object, graph, options = null) {
693
+ return new DatasetCoreAndReadableStream(this, subject, predicate, object, graph, {
694
+ entityIndex: this._entityIndex,
695
+ matchSemantics: options && options.matchSemantics !== undefined ?
696
+ options.matchSemantics : this._matchSemantics,
697
+ });
632
698
  }
633
699
 
634
700
  // ### `countQuads` returns the number of quads matching a pattern.
@@ -957,7 +1023,8 @@ export default class N3Store {
957
1023
 
958
1024
  if (Array.isArray(quads))
959
1025
  this.addQuads(quads);
960
- else if (quads instanceof N3Store && quads._entityIndex === this._entityIndex) {
1026
+ // Index merging bypasses observer notifications
1027
+ else if (this._observers === null && quads instanceof N3Store && quads._entityIndex === this._entityIndex) {
961
1028
  if (quads._size !== 0) {
962
1029
  this._graphs = merge(this._graphs, quads._graphs);
963
1030
  this._size = null; // Invalidate the cached size
@@ -1015,7 +1082,7 @@ export default class N3Store {
1015
1082
  * @param graph The optional exact graph to match.
1016
1083
  */
1017
1084
  deleteMatches(subject, predicate, object, graph) {
1018
- for (const quad of this.match(subject, predicate, object, graph))
1085
+ for (const quad of this.match(subject, predicate, object, graph, { matchSemantics: 'lazy' }))
1019
1086
  this.removeQuad(quad);
1020
1087
  return this;
1021
1088
  }
@@ -1143,7 +1210,7 @@ export default class N3Store {
1143
1210
  * Returns a stream that contains all quads of the dataset.
1144
1211
  */
1145
1212
  toStream() {
1146
- return this.match();
1213
+ return this.match(null, null, null, null, { matchSemantics: 'lazy' });
1147
1214
  }
1148
1215
 
1149
1216
  /**
@@ -1203,6 +1270,98 @@ function indexMatch(index, ids, depth = 0) {
1203
1270
  return target;
1204
1271
  }
1205
1272
 
1273
+ // Capture a read in the same index order as readQuads, without constructing RDF terms.
1274
+ // A flat list avoids allocating a Quad (and its terms) for every unread result.
1275
+ function snapshotMatch(store, subject, predicate, object, graph) {
1276
+ const snapshot = { store, ids: [] };
1277
+ const subjectId = subject && store._termToNumericId(subject);
1278
+ const predicateId = predicate && store._termToNumericId(predicate);
1279
+ const objectId = object && store._termToNumericId(object);
1280
+ // Only active reads are frozen: bound terms and graphs have already resolved,
1281
+ // and notifications run before deletion. Entity IDs are never removed.
1282
+
1283
+ // Keep this choice aligned with readQuads: changing order would repeat or skip
1284
+ // results when an iterator resumes partway through its snapshot.
1285
+ let indexName, key0, key1, key2, positions;
1286
+ if (objectId && (subjectId || !predicateId)) {
1287
+ indexName = 'objects';
1288
+ [key0, key1, key2] = [objectId, subjectId, predicateId];
1289
+ positions = [2, 0, 1];
1290
+ }
1291
+ else if (!subjectId && predicateId) {
1292
+ indexName = 'predicates';
1293
+ [key0, key1, key2] = [predicateId, objectId, subjectId];
1294
+ positions = [1, 2, 0];
1295
+ }
1296
+ else {
1297
+ indexName = 'subjects';
1298
+ [key0, key1, key2] = [subjectId, predicateId, objectId];
1299
+ positions = [0, 1, 2];
1300
+ }
1301
+
1302
+ const graphs = store._getGraphs(graph), parts = [];
1303
+ for (const graphId in graphs) {
1304
+ const index = graphs[graphId][indexName];
1305
+ const graphKey = Number(graphId);
1306
+ for (const value0 in (key0 ? { [key0]: index[key0] } : index)) {
1307
+ const index1 = index[value0];
1308
+ if (!index1) continue; // eslint-disable-line no-continue
1309
+ parts[positions[0]] = Number(value0);
1310
+ for (const value1 in (key1 ? { [key1]: index1[key1] } : index1)) {
1311
+ const index2 = index1[value1];
1312
+ if (!index2) continue; // eslint-disable-line no-continue
1313
+ parts[positions[1]] = Number(value1);
1314
+ for (const value2 in (key2 ? (key2 in index2 ? { [key2]: null } : {}) : index2)) {
1315
+ parts[positions[2]] = Number(value2);
1316
+ snapshot.ids.push(parts[0], parts[1], parts[2], graphKey);
1317
+ }
1318
+ }
1319
+ }
1320
+ }
1321
+ return snapshot;
1322
+ }
1323
+
1324
+ // Skip already-yielded IDs in constant time; only materialize the requested tail.
1325
+ function* iterateSnapshot({ store, ids }, offset) {
1326
+ const entities = store._entities;
1327
+ let subject, predicate, object, graph;
1328
+ let subjectId, predicateId, objectId, graphId;
1329
+ for (let i = offset * 4; i < ids.length; i += 4) {
1330
+ if (subjectId !== ids[i])
1331
+ subject = store._termFromId(entities[subjectId = ids[i]]);
1332
+ if (predicateId !== ids[i + 1])
1333
+ predicate = store._termFromId(entities[predicateId = ids[i + 1]]);
1334
+ if (objectId !== ids[i + 2])
1335
+ object = store._termFromId(entities[objectId = ids[i + 2]]);
1336
+ if (graphId !== ids[i + 3])
1337
+ graph = store._termFromId(entities[graphId = ids[i + 3]]);
1338
+ yield store._factory.quad(subject, predicate, object, graph);
1339
+ }
1340
+ }
1341
+
1342
+ function validateMatchSemantics(semantics = 'lazy') {
1343
+ if (semantics !== 'lazy' && semantics !== 'snapshot' && semantics !== 'forwarded')
1344
+ throw new Error(`Unknown matchSemantics: ${semantics}`);
1345
+ if (semantics !== 'lazy' && (typeof WeakRef !== 'function' || typeof FinalizationRegistry !== 'function'))
1346
+ throw new Error('Non-lazy matchSemantics requires WeakRef and FinalizationRegistry support');
1347
+ return semantics;
1348
+ }
1349
+
1350
+ // Returns the intersection of two quad patterns, or false if they conflict.
1351
+ function intersectMatchPatterns(left, right) {
1352
+ const result = new Array(4);
1353
+ for (let i = 0; i < 4; i++) {
1354
+ const leftTerm = left[i], rightTerm = right[i];
1355
+ if (leftTerm === null || leftTerm === undefined)
1356
+ result[i] = rightTerm;
1357
+ else if (rightTerm === null || rightTerm === undefined || termToId(leftTerm) === termToId(rightTerm))
1358
+ result[i] = leftTerm;
1359
+ else
1360
+ return false;
1361
+ }
1362
+ return result;
1363
+ }
1364
+
1206
1365
  /**
1207
1366
  * A class that implements both DatasetCore and Readable.
1208
1367
  */
@@ -1210,13 +1369,103 @@ class DatasetCoreAndReadableStream extends Readable {
1210
1369
  constructor(n3Store, subject, predicate, object, graph, options) {
1211
1370
  super({ objectMode: true });
1212
1371
  Object.assign(this, { n3Store, subject, predicate, object, graph, options });
1372
+ const semantics = this._semantics = validateMatchSemantics(options.matchSemantics);
1373
+
1374
+ if (options.matchesNothing) {
1375
+ this._matchesNothing = true;
1376
+ this._filtered = new N3Store({ factory: n3Store._factory, entityIndex: options.entityIndex });
1377
+ }
1378
+
1379
+ if (semantics !== 'lazy') {
1380
+ // Active reads share a snapshot only when their source changes.
1381
+ this._readers = null;
1382
+ // Cache pattern ids on first use
1383
+ this._subjectId = this._predicateId = this._objectId = this._graphId = undefined;
1384
+ if (!this._matchesNothing) {
1385
+ this._observer = n3Store._addObserver(this);
1386
+ }
1387
+ }
1388
+ }
1389
+
1390
+ // ### `_matchesPattern` tests quad ids against this view.
1391
+ _matchesPattern(subjectId, predicateId, objectId, graphId) {
1392
+ const { subject, predicate, object, graph, n3Store } = this;
1393
+ if (subject && subjectId !== (this._subjectId || (this._subjectId = n3Store._termToNumericId(subject))))
1394
+ return false;
1395
+ if (predicate && predicateId !== (this._predicateId || (this._predicateId = n3Store._termToNumericId(predicate))))
1396
+ return false;
1397
+ if (object && objectId !== (this._objectId || (this._objectId = n3Store._termToNumericId(object))))
1398
+ return false;
1399
+ return graph === null || graph === undefined ||
1400
+ graphId === (this._graphId || (this._graphId =
1401
+ graph === '' || isDefaultGraph(graph) ? 1 : n3Store._termToNumericId(graph)));
1402
+ }
1403
+
1404
+ // ### `_matchesQuad` tests a Quad against this view.
1405
+ _matchesQuad(quad) {
1406
+ const { subject, predicate, object, graph } = this;
1407
+ return !this._matchesNothing &&
1408
+ (subject === null || subject === undefined || termToId(subject) === termToId(quad.subject)) &&
1409
+ (predicate === null || predicate === undefined || termToId(predicate) === termToId(quad.predicate)) &&
1410
+ (object === null || object === undefined || termToId(object) === termToId(quad.object)) &&
1411
+ (graph === null || graph === undefined || termToId(graph) === termToId(quad.graph));
1412
+ }
1413
+
1414
+ // ### `_assertMatchesPattern` rejects a Quad outside this view.
1415
+ _assertMatchesPattern(quad) {
1416
+ if (!this._matchesQuad(quad))
1417
+ throw new Error('Quad does not match the forwarded view pattern');
1418
+ }
1419
+
1420
+ // ### `_sourceIterator` returns an iterator over the current backing store.
1421
+ _sourceIterator() {
1422
+ return this._filtered ? this._filtered[Symbol.iterator]() :
1423
+ this.n3Store.readQuads(this.subject, this.predicate, this.object, this.graph);
1424
+ }
1425
+
1426
+ // ### `_freezeCurrentIterators` freezes only readers still using the live source.
1427
+ _freezeCurrentIterators() {
1428
+ if (this._readers) {
1429
+ this._readers.snapshot = this._filtered ? snapshotMatch(this._filtered) :
1430
+ snapshotMatch(this.n3Store, this.subject, this.predicate, this.object, this.graph);
1431
+ // Each iterator retains its own group. New reads must see the new source;
1432
+ // the view must not retain snapshots belonging to suspended or abandoned reads.
1433
+ this._readers = null;
1434
+ }
1435
+ }
1436
+
1437
+ // ### `_onParentMutation` applies a parent mutation to this view.
1438
+ _onParentMutation(subjectId, predicateId, objectId, graphId, added) {
1439
+ if (!this._matchesPattern(subjectId, predicateId, objectId, graphId))
1440
+ return;
1441
+
1442
+ this._freezeCurrentIterators();
1443
+
1444
+ // Keep using the parent until materialization
1445
+ if (this._semantics === 'forwarded') {
1446
+ if (this._filtered) {
1447
+ if (added)
1448
+ this._filtered._addQuad(subjectId, predicateId, objectId, graphId);
1449
+ else
1450
+ this._filtered._removeQuad(subjectId, predicateId, objectId, graphId);
1451
+ }
1452
+ return;
1453
+ }
1454
+
1455
+ // Capture the pre-mutation snapshot
1456
+ this._filtered = this.filtered;
1213
1457
  }
1214
1458
 
1215
1459
  get filtered() {
1216
1460
  if (!this._filtered) {
1217
1461
  const { n3Store, graph, object, predicate, subject } = this;
1462
+ if (this._semantics !== 'lazy')
1463
+ this._freezeCurrentIterators();
1218
1464
  const newStore = this._filtered = new N3Store({ factory: n3Store._factory, entityIndex: this.options.entityIndex });
1219
1465
 
1466
+ if (this._semantics === 'snapshot')
1467
+ this._detachObserver();
1468
+
1220
1469
  let subjectId, predicateId, objectId;
1221
1470
 
1222
1471
  // Translate IRIs to internal index keys.
@@ -1273,7 +1522,42 @@ class DatasetCoreAndReadableStream extends Readable {
1273
1522
  }
1274
1523
  }
1275
1524
 
1525
+ // ### `_destroy` closes the cached iterator.
1526
+ _destroy(error, callback) {
1527
+ if (this[ITERATOR]) {
1528
+ this[ITERATOR].return();
1529
+ this[ITERATOR] = null;
1530
+ }
1531
+ callback(error);
1532
+ }
1533
+
1534
+ // ### `_detachObserver` stops observing the parent store.
1535
+ _detachObserver() {
1536
+ if (this._observer) {
1537
+ this.n3Store._removeObserver(this._observer);
1538
+ this._observer = null;
1539
+ }
1540
+ }
1541
+
1542
+ // ### `_detach` freezes the view and stops observing the parent.
1543
+ _detach() {
1544
+ this._filtered = this.filtered;
1545
+ this._detachObserver();
1546
+ // Lazy views deferred this state
1547
+ if (this._semantics === 'lazy')
1548
+ this._readers = null;
1549
+ this._semantics = 'snapshot';
1550
+ return this;
1551
+ }
1552
+
1276
1553
  addAll(quads) {
1554
+ if (this._semantics === 'forwarded') {
1555
+ for (const quad of quads) {
1556
+ this._assertMatchesPattern(quad);
1557
+ this.n3Store.addQuad(quad);
1558
+ }
1559
+ return this;
1560
+ }
1277
1561
  return this.filtered.addAll(quads);
1278
1562
  }
1279
1563
 
@@ -1282,6 +1566,19 @@ class DatasetCoreAndReadableStream extends Readable {
1282
1566
  }
1283
1567
 
1284
1568
  deleteMatches(subject, predicate, object, graph) {
1569
+ if (this._semantics === 'forwarded') {
1570
+ // No deletion pattern can match a view with conflicting ancestor patterns.
1571
+ if (this._matchesNothing)
1572
+ throw new Error('Cannot delete from a conflicting forwarded view pattern');
1573
+ const pattern = intersectMatchPatterns(
1574
+ [this.subject, this.predicate, this.object, this.graph],
1575
+ [subject, predicate, object, graph],
1576
+ );
1577
+ if (!pattern)
1578
+ throw new Error('Deletion pattern does not match the forwarded view pattern');
1579
+ this.n3Store.deleteMatches(...pattern);
1580
+ return this;
1581
+ }
1285
1582
  return this.filtered.deleteMatches(subject, predicate, object, graph);
1286
1583
  }
1287
1584
 
@@ -1294,19 +1591,41 @@ class DatasetCoreAndReadableStream extends Readable {
1294
1591
  }
1295
1592
 
1296
1593
  every(callback, subject, predicate, object, graph) {
1297
- return this.filtered.every(callback, subject, predicate, object, graph);
1594
+ return this.filtered.every(this._semantics === 'forwarded' ?
1595
+ quad => callback(quad, this) : callback, subject, predicate, object, graph);
1298
1596
  }
1299
1597
 
1300
1598
  filter(iteratee) {
1301
- return this.filtered.filter(iteratee);
1599
+ return this.filtered.filter(this._semantics === 'forwarded' ?
1600
+ quad => iteratee(quad, this) : iteratee);
1302
1601
  }
1303
1602
 
1304
1603
  forEach(callback, subject, predicate, object, graph) {
1305
- return this.filtered.forEach(callback, subject, predicate, object, graph);
1604
+ return this.filtered.forEach(this._semantics === 'forwarded' ?
1605
+ quad => callback(quad, this) : callback, subject, predicate, object, graph);
1306
1606
  }
1307
1607
 
1308
1608
  import(stream) {
1309
- return this.filtered.import(stream);
1609
+ if (this._semantics !== 'forwarded')
1610
+ return this.filtered.import(stream);
1611
+
1612
+ const view = this;
1613
+ function onData(quad) {
1614
+ try {
1615
+ view._assertMatchesPattern(quad);
1616
+ view.n3Store.addQuad(quad);
1617
+ }
1618
+ catch (error) {
1619
+ // Stop this import; RDF/JS streams need not implement destroy()
1620
+ stream.removeListener('data', onData);
1621
+ if (typeof stream.destroy === 'function')
1622
+ stream.destroy(error);
1623
+ else
1624
+ stream.emit('error', error);
1625
+ }
1626
+ }
1627
+ stream.on('data', onData);
1628
+ return stream;
1310
1629
  }
1311
1630
 
1312
1631
  intersection(other) {
@@ -1314,11 +1633,13 @@ class DatasetCoreAndReadableStream extends Readable {
1314
1633
  }
1315
1634
 
1316
1635
  map(iteratee) {
1317
- return this.filtered.map(iteratee);
1636
+ return this.filtered.map(this._semantics === 'forwarded' ?
1637
+ quad => iteratee(quad, this) : iteratee);
1318
1638
  }
1319
1639
 
1320
1640
  some(callback, subject, predicate, object, graph) {
1321
- return this.filtered.some(callback, subject, predicate, object, graph);
1641
+ return this.filtered.some(this._semantics === 'forwarded' ?
1642
+ quad => callback(quad, this) : callback, subject, predicate, object, graph);
1322
1643
  }
1323
1644
 
1324
1645
  toCanonical() {
@@ -1326,23 +1647,29 @@ class DatasetCoreAndReadableStream extends Readable {
1326
1647
  }
1327
1648
 
1328
1649
  toStream() {
1650
+ if (this._semantics !== 'lazy')
1651
+ // Use a fresh sync iterator instead of consuming this view's own readable stream.
1652
+ return Readable.from(this[Symbol.iterator]());
1329
1653
  return this._filtered ?
1330
1654
  this._filtered.toStream()
1331
- : this.n3Store.match(this.subject, this.predicate, this.object, this.graph);
1655
+ : this.n3Store.match(this.subject, this.predicate, this.object, this.graph, { matchSemantics: 'lazy' });
1332
1656
  }
1333
1657
 
1334
1658
  union(quads) {
1335
1659
  return this._filtered ?
1336
1660
  this._filtered.union(quads)
1337
- : this.n3Store.match(this.subject, this.predicate, this.object, this.graph).addAll(quads);
1661
+ : this.n3Store.match(this.subject, this.predicate, this.object, this.graph, { matchSemantics: 'lazy' }).addAll(quads);
1338
1662
  }
1339
1663
 
1340
1664
  toArray() {
1665
+ if (this._semantics !== 'lazy')
1666
+ return [...this];
1341
1667
  return this._filtered ? this._filtered.toArray() : this.n3Store.getQuads(this.subject, this.predicate, this.object, this.graph);
1342
1668
  }
1343
1669
 
1344
1670
  reduce(callback, initialValue) {
1345
- return this.filtered.reduce(callback, initialValue);
1671
+ return this.filtered.reduce(this._semantics === 'forwarded' ?
1672
+ (accumulator, quad) => callback(accumulator, quad, this) : callback, initialValue);
1346
1673
  }
1347
1674
 
1348
1675
  toString() {
@@ -1350,10 +1677,20 @@ class DatasetCoreAndReadableStream extends Readable {
1350
1677
  }
1351
1678
 
1352
1679
  add(quad) {
1680
+ if (this._semantics === 'forwarded') {
1681
+ this._assertMatchesPattern(quad);
1682
+ this.n3Store.addQuad(quad);
1683
+ return this;
1684
+ }
1353
1685
  return this.filtered.add(quad);
1354
1686
  }
1355
1687
 
1356
1688
  delete(quad) {
1689
+ if (this._semantics === 'forwarded') {
1690
+ this._assertMatchesPattern(quad);
1691
+ this.n3Store.removeQuad(quad);
1692
+ return this;
1693
+ }
1357
1694
  return this.filtered.delete(quad);
1358
1695
  }
1359
1696
 
@@ -1361,12 +1698,61 @@ class DatasetCoreAndReadableStream extends Readable {
1361
1698
  return this.filtered.has(quad);
1362
1699
  }
1363
1700
 
1364
- match(subject, predicate, object, graph) {
1365
- return new DatasetCoreAndReadableStream(this.filtered, subject, predicate, object, graph, this.options);
1701
+ match(subject, predicate, object, graph, options = null) {
1702
+ const requestedSemantics = options && options.matchSemantics;
1703
+ if (options && requestedSemantics !== undefined) {
1704
+ validateMatchSemantics(requestedSemantics);
1705
+ if (requestedSemantics !== this._semantics)
1706
+ throw new Error(`Cannot override matchSemantics on a view: inherited "${this._semantics}", received "${requestedSemantics}"`);
1707
+ }
1708
+
1709
+ if (this._semantics !== 'forwarded')
1710
+ return new DatasetCoreAndReadableStream(this.filtered, subject, predicate, object, graph, {
1711
+ entityIndex: this.options.entityIndex,
1712
+ matchSemantics: this._semantics,
1713
+ });
1714
+
1715
+ const pattern = !this._matchesNothing && intersectMatchPatterns(
1716
+ [this.subject, this.predicate, this.object, this.graph],
1717
+ [subject, predicate, object, graph],
1718
+ );
1719
+ const [matchedSubject, matchedPredicate, matchedObject, matchedGraph] = pattern || [null, null, null, null];
1720
+ return new DatasetCoreAndReadableStream(
1721
+ this.n3Store, matchedSubject, matchedPredicate, matchedObject, matchedGraph, {
1722
+ entityIndex: this.options.entityIndex,
1723
+ matchSemantics: 'forwarded',
1724
+ matchesNothing: !pattern,
1725
+ });
1366
1726
  }
1367
1727
 
1368
1728
  [Symbol.iterator]() {
1369
- return this._filtered ? this._filtered[Symbol.iterator]() :
1370
- this.n3Store.readQuads(this.subject, this.predicate, this.object, this.graph);
1729
+ return this._semantics === 'lazy' ? this._sourceIterator() : this._iterateStable();
1730
+ }
1731
+
1732
+ *_iterateStable() {
1733
+ const readers = this._readers || (this._readers = { count: 0, snapshot: null });
1734
+ const source = this._sourceIterator();
1735
+ let yielded = 0;
1736
+ readers.count++;
1737
+ try {
1738
+ while (!readers.snapshot) {
1739
+ const { value, done } = source.next();
1740
+ // A custom factory can mutate the store inside next(), even on its first call.
1741
+ if (readers.snapshot || done)
1742
+ break;
1743
+ yielded++;
1744
+ yield value;
1745
+ }
1746
+ if (readers.snapshot)
1747
+ yield* iterateSnapshot(readers.snapshot, yielded);
1748
+ }
1749
+ finally {
1750
+ source.return();
1751
+ if (--readers.count === 0) {
1752
+ readers.snapshot = null;
1753
+ if (this._readers === readers)
1754
+ this._readers = null;
1755
+ }
1756
+ }
1371
1757
  }
1372
1758
  }