Changeset - df9a6c457587
[Not reviewed]
default
0 4 0
drewp@bigasterisk.com - 20 months ago 2023-05-29 22:15:19
drewp@bigasterisk.com
convert Patch to use Immutable
4 files changed with 75 insertions and 49 deletions:
0 comments (0 inline, 0 general)
light9/web/patch.test.ts
Show inline comments
 
@@ -34,12 +34,22 @@ describe("Patch.matches", () => {
 
    assert.isFalse(p.matches(QP(null, null, null, null)));
 
  });
 
  it("compares terms correctly", () => {
 
    assert.isTrue(new Patch([new Quad(node1, node2, node3)], []).matches(QP(node1, null, null, null)));
 
    assert.isFalse(new Patch([new Quad(node1, node2, node3)], []).matches(QP(node2, null, null, null)));
 
  });
 
  it("matches on just one set term", () => {
 
    assert.isTrue(new Patch([new Quad(node1, node2, node3)], []).matches(QP(node1, null, null, null)));
 
    assert.isTrue(new Patch([new Quad(node1, node2, node3)], []).matches(QP(null, node2, null, null)));
 
    assert.isTrue(new Patch([new Quad(node1, node2, node3)], []).matches(QP(null, null, node3, null)));
 
  });
 
});
 
describe("Patch.empty", () => {
 
  it("works with no quads", () => {
 
    const p = new Patch([], []);
 
    assert.isTrue(p.isEmpty());
 
  });
 
  it("works with unmatched quads", () => {
 
    const p = new Patch([], [new Quad(node1, node2, node3)]);
 
    assert.isFalse(p.isEmpty());
 
  });
 
});
light9/web/patch.ts
Show inline comments
 
import * as async from "async";
 
import debug from "debug";
 
import * as N3 from "n3";
 
import { NamedNode, Parser, Quad, Writer } from "n3";
 

	
 
import * as Immutable from "immutable";
 
export interface QuadPattern {
 
  subject: N3.Quad_Subject | null;
 
  predicate: N3.Quad_Predicate | null;
 
  object: N3.Quad_Object | null; // literals allowed? needs review. probably 'yes'.
 
  graph: N3.Quad_Graph | null;
 
}
 

	
 
const log = debug("patch");
 

	
 
export class Patch {
 
  // immutable
 
  private _allPredsCache?: Set<string>;
 
  private _allSubjsCache?: Set<string>;
 
  constructor(private dels: Quad[], private adds: Quad[]) {
 
  private dels: Immutable.Set<Quad>;
 
  private adds: Immutable.Set<Quad>;
 
  private _allPredsCache?: Immutable.Set<string>;
 
  private _allSubjsCache?: Immutable.Set<string>;
 
  constructor(dels: Iterable<Quad>, adds: Iterable<Quad>) {
 
    this.dels = Immutable.Set(dels);
 
    this.adds = Immutable.Set(adds);
 
    this.validate();
 
  }
 

	
 
  private validate() {
 
    // todo: finish porting this from coffeescript
 
    [this.adds, this.dels].map((qs: Quad[]) =>
 
      (() => {
 
        const result = [];
 
        for (let q of Array.from(qs)) {
 
          if (!q.equals) {
 
            throw new Error("doesn't look like a proper Quad");
 
          }
 
          if (!q.subject.id || q.graph.id == null || q.predicate.id == null) {
 
            throw new Error(`corrupt patch: ${JSON.stringify(q)}`);
 
          } else {
 
            result.push(undefined);
 
          }
 
        }
 
        return result;
 
      })()
 
    );
 
    this.adds.union(this.dels).forEach((q: Quad) => {
 
      if (!q.equals) {
 
        throw new Error("doesn't look like a proper Quad");
 
      }
 
      if (!q.subject.id || q.graph.id == null || q.predicate.id == null) {
 
        throw new Error(`corrupt patch: ${JSON.stringify(q)}`);
 
      }
 
      if (
 
        q.object.termType == "Literal" &&
 
        (q.object.datatypeString == "http://www.w3.org/2001/XMLSchema#float" || q.object.datatypeString == "http://www.w3.org/2001/XMLSchema#double")
 
      ) {
 
        throw new Error(`${JSON.stringify(q)} is using non-decimal for numbers, which is going to break some comparisons`);
 
      }
 
    });
 
  }
 

	
 
  matches(pat: QuadPattern): boolean {
 
    const allQuads = this.dels.concat(this.adds);
 
    return allQuads.some((quad) => {
 
      return (
 
        (pat.subject === null || pat.subject.equals(quad.subject)) && //
 
        (pat.predicate === null || pat.predicate.equals(quad.predicate)) && //
 
        (pat.object === null || pat.object.equals(quad.object)) && //
 
        (pat.graph === null || pat.graph.equals(quad.graph))
 
      );
 
    });
 
  }
 

	
 
  isEmpty() {
 
    // sometimes returns bogus false- waiting for port to Immutable
 
    return !this.dels.length && !this.adds.length;
 
    return Immutable.is(this.dels, this.adds);
 
  }
 

	
 
  applyToGraph(g: N3.Store) {
 
    for (let quad of Array.from(this.dels)) {
 
    for (let quad of this.dels) {
 
      g.removeQuad(quad);
 
    }
 
    for (let quad of Array.from(this.adds)) {
 
    for (let quad of this.adds) {
 
      g.addQuad(quad);
 
    }
 
  }
 

	
 
  update(other: Patch): Patch {
 
    // this is approx, since it doesnt handle matching existing quads.
 
    return new Patch(this.dels.concat(other.dels), this.adds.concat(other.adds));
 
    // this is approx, since it doesnt handle cancelling existing quads.
 
    return new Patch(this.dels.union(other.dels), this.adds.union(other.adds));
 
  }
 

	
 
  summary(): string {
 
    return "-" + this.dels.length + " +" + this.adds.length;
 
    return "-" + this.dels.size + " +" + this.adds.size;
 
  }
 

	
 
  dump(): string {
 
    const lines: string[] = [];
 
    const s = (term: N3.Term): string => {
 
      if (term.termType == "Literal") return term.value;
 
      if (term.termType == "NamedNode")
 
        return term.value
 
          .replace("http://light9.bigasterisk.com/effect/", "effect:")
 
          .replace("http://light9.bigasterisk.com/", ":")
 
          .replace("http://www.w3.org/2000/01/rdf-schema#", "rdfs:")
 
          .replace("http://www.w3.org/1999/02/22-rdf-syntax-ns#", "rdf:");
 
@@ -92,84 +94,91 @@ export class Patch {
 
    this.dels.forEach((d) => lines.push(delPrefix + s(d.subject) + " " + s(d.predicate) + " " + s(d.object)));
 
    this.adds.forEach((d) => lines.push(addPrefix + s(d.subject) + " " + s(d.predicate) + " " + s(d.object)));
 
    lines.sort();
 
    return lines.join("\n");
 
  }
 

	
 
  async toJsonPatch(): Promise<string> {
 
    return new Promise((res, rej) => {
 
      const out: SyncgraphPatchMessage = { patch: { adds: "", deletes: "" } };
 

	
 
      const writeDels = (cb1: () => void) => {
 
        const writer = new Writer({ format: "N-Quads" });
 
        writer.addQuads(this.dels);
 
        writer.addQuads(this.dels.toArray());
 
        writer.end(function (err: any, result: string) {
 
          out.patch.deletes = result;
 
          cb1();
 
        });
 
      };
 

	
 
      const writeAdds = (cb2: () => void) => {
 
        const writer = new Writer({ format: "N-Quads" });
 
        writer.addQuads(this.adds);
 
        writer.addQuads(this.adds.toArray());
 
        writer.end(function (err: any, result: string) {
 
          out.patch.adds = result;
 
          cb2();
 
        });
 
      };
 

	
 
      async.parallel([writeDels, writeAdds], (err: any) => res(JSON.stringify(out)));
 
    });
 
  }
 

	
 
  containsAnyPreds(preds: NamedNode[]): boolean {
 
  containsAnyPreds(preds: Iterable<NamedNode>): boolean {
 
    if (this._allPredsCache === undefined) {
 
      this._allPredsCache = new Set();
 
      for (let qq of [this.adds, this.dels]) {
 
        for (let q of Array.from(qq)) {
 
          this._allPredsCache.add(q.predicate.value);
 
      this._allPredsCache = Immutable.Set();
 
      this._allPredsCache.withMutations((cache) => {
 
        for (let qq of [this.adds, this.dels]) {
 
          for (let q of Array.from(qq)) {
 
            cache.add(q.predicate.value);
 
          }
 
        }
 
      }
 
      });
 
    }
 

	
 
    for (let p of Array.from(preds)) {
 
    for (let p of preds) {
 
      if (this._allPredsCache.has(p.value)) {
 
        return true;
 
      }
 
    }
 
    return false;
 
  }
 

	
 
  allSubjs(): Set<string> {
 
  allSubjs(): Immutable.Set<string> {
 
    // returns subjs as Set of strings
 
    if (this._allSubjsCache === undefined) {
 
      this._allSubjsCache = new Set();
 
      for (let qq of [this.adds, this.dels]) {
 
        for (let q of Array.from(qq)) {
 
          this._allSubjsCache.add(q.subject.value);
 
      this._allSubjsCache = Immutable.Set();
 
      this._allSubjsCache.withMutations((cache) => {
 
        for (let qq of [this.adds, this.dels]) {
 
          for (let q of Array.from(qq)) {
 
            cache.add(q.subject.value);
 
          }
 
        }
 
      }
 
      });
 
    }
 

	
 
    return this._allSubjsCache;
 
  }
 

	
 
  allPreds(): Set<NamedNode> {
 
    const ret = new Set<NamedNode>();
 
    for (let qq of [this.adds, this.dels]) {
 
      for (let q of Array.from(qq)) {
 
        if (q.predicate.termType == "Variable") throw "unsupported";
 
        ret.add(q.predicate);
 
  allPreds(): Immutable.Set<NamedNode> {
 
    // todo: this could cache
 
    const ret = Immutable.Set<NamedNode>();
 
    ret.withMutations((r) => {
 
      for (let qq of [this.adds, this.dels]) {
 
        for (let q of Array.from(qq)) {
 
          if (q.predicate.termType == "Variable") throw "unsupported";
 
          r.add(q.predicate);
 
        }
 
      }
 
    }
 
    });
 
    return ret;
 
  }
 
}
 

	
 
// The schema of the json sent from graph server.
 
export interface SyncgraphPatchMessage {
 
  patch: { adds: string; deletes: string };
 
}
 

	
 
export function patchToDeleteEntireGraph(g: N3.Store) {
 
  return new Patch(g.getQuads(null, null, null, null), []);
 
}
package.json
Show inline comments
 
@@ -14,24 +14,25 @@
 
    "@types/async": "^3.2.20",
 
    "@types/d3": "^7.4.0",
 
    "@types/debug": "^4.1.7",
 
    "@types/fpsmeter": "^0.3.31",
 
    "@types/n3": "^1.10.4",
 
    "@types/node": "^20.2.0",
 
    "@types/reconnectingwebsocket": "^1.0.7",
 
    "@types/underscore": "^1.11.4",
 
    "async": "^3.2.4",
 
    "d3": "^7.8.4",
 
    "debug": "^4.3.4",
 
    "fpsmeter": "^0.3.1",
 
    "immutable": "^4.3.0",
 
    "knockout": "^3.5.1",
 
    "lit": "^2.7.4",
 
    "n3": "^1.16.4",
 
    "onecolor": "^4.0.0",
 
    "parse-prometheus-text-format": "^1.1.1",
 
    "reconnectingwebsocket": "^1.0.0",
 
    "sub-events": "^1.9.0",
 
    "sylvester": "^0.0.21",
 
    "typescript": "^5.0.4",
 
    "underscore": "^1.13.6",
 
    "vite": "^4.3.7",
 
    "vite-plugin-rewrite-all": "^1.0.1",
pnpm-lock.yaml
Show inline comments
 
@@ -6,24 +6,25 @@ specifiers:
 
  '@types/async': ^3.2.20
 
  '@types/d3': ^7.4.0
 
  '@types/debug': ^4.1.7
 
  '@types/fpsmeter': ^0.3.31
 
  '@types/n3': ^1.10.4
 
  '@types/node': ^20.2.0
 
  '@types/reconnectingwebsocket': ^1.0.7
 
  '@types/underscore': ^1.11.4
 
  async: ^3.2.4
 
  d3: ^7.8.4
 
  debug: ^4.3.4
 
  fpsmeter: ^0.3.1
 
  immutable: ^4.3.0
 
  knockout: ^3.5.1
 
  lit: ^2.7.4
 
  n3: ^1.16.4
 
  onecolor: ^4.0.0
 
  parse-prometheus-text-format: ^1.1.1
 
  reconnectingwebsocket: ^1.0.0
 
  sub-events: ^1.9.0
 
  sylvester: ^0.0.21
 
  typescript: ^5.0.4
 
  underscore: ^1.13.6
 
  vite: ^4.3.7
 
  vite-plugin-rewrite-all: ^1.0.1
 
@@ -35,24 +36,25 @@ dependencies:
 
  '@types/async': 3.2.20
 
  '@types/d3': 7.4.0
 
  '@types/debug': 4.1.7
 
  '@types/fpsmeter': 0.3.31
 
  '@types/n3': 1.10.4
 
  '@types/node': 20.2.0
 
  '@types/reconnectingwebsocket': 1.0.7
 
  '@types/underscore': 1.11.4
 
  async: 3.2.4
 
  d3: 7.8.4
 
  debug: 4.3.4
 
  fpsmeter: 0.3.1
 
  immutable: 4.3.0
 
  knockout: 3.5.1
 
  lit: 2.7.4
 
  n3: 1.16.4
 
  onecolor: 4.0.0
 
  parse-prometheus-text-format: 1.1.1
 
  reconnectingwebsocket: 1.0.0
 
  sub-events: 1.9.0
 
  sylvester: 0.0.21
 
  typescript: 5.0.4
 
  underscore: 1.13.6
 
  vite: 4.3.7_@types+node@20.2.0
 
  vite-plugin-rewrite-all: 1.0.1_vite@4.3.7
 
@@ -1147,24 +1149,28 @@ packages:
 

	
 
  /iconv-lite/0.6.3:
 
    resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
 
    engines: {node: '>=0.10.0'}
 
    dependencies:
 
      safer-buffer: 2.1.2
 
    dev: false
 

	
 
  /ieee754/1.2.1:
 
    resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
 
    dev: false
 

	
 
  /immutable/4.3.0:
 
    resolution: {integrity: sha512-0AOCmOip+xgJwEVTQj1EfiDDOkPmuyllDuTuEX+DDXUgapLAsBIfkg3sxCYyCEA8mQqZrrxPUGjcOQ2JS3WLkg==}
 
    dev: false
 

	
 
  /internmap/2.0.3:
 
    resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==}
 
    engines: {node: '>=12'}
 
    dev: false
 

	
 
  /js-string-escape/1.0.1:
 
    resolution: {integrity: sha512-Smw4xcfIQ5LVjAOuJCvN/zIodzA/BBSsluuoSykP+lUvScIi4U6RJLfwHet5cxFnCswUjISV8oAXaqaJDY3chg==}
 
    engines: {node: '>= 0.8'}
 
    dev: false
 

	
 
  /jsonc-parser/3.2.0:
 
    resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==}
0 comments (0 inline, 0 general)