diff --git a/light9/web/graph.coffee b/light9/web/graph.ts rename from light9/web/graph.coffee rename to light9/web/graph.ts --- a/light9/web/graph.coffee +++ b/light9/web/graph.ts @@ -1,430 +1,531 @@ -log = debug('graph') - -# Patch is {addQuads: , delQuads: } -# are made with Quad(s,p,o,g) - -# for mocha -if require? - `window = {}` - `_ = require('./lib/underscore/underscore-min.js')` - `N3 = require('../../node_modules/n3/n3-browser.js')` - `d3 = require('../../node_modules/d3/dist/d3.min.js')` - `RdfDbClient = require('./rdfdbclient.js').RdfDbClient` - module.exports = window +import * as d3 from "d3"; +import debug from "debug"; +import * as N3 from "n3"; +import { Quad, Quad_Subject, Quad_Predicate, Quad_Object, Quad_Graph } from "n3"; +import { filter, sortBy, unique } from "underscore"; +import { allPatchSubjs, Patch } from "./patch"; +import { RdfDbClient } from "./rdfdbclient"; +const log = debug("graph"); -RDF = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#' - -patchSizeSummary = (patch) -> - '-' + patch.delQuads.length + ' +' + patch.addQuads.length +const RDF = "http://www.w3.org/1999/02/22-rdf-syntax-ns#"; -# (sloppily shared to rdfdbclient.coffee too) -window.patchSizeSummary = patchSizeSummary - -patchContainsPreds = (patch, preds) -> - if patch._allPreds == undefined - patch._allPreds = new Set() - for qq in [patch.addQuads, patch.delQuads] - for q in qq - patch._allPreds.add(q.predicate.value) +interface QuadPattern { + subject: Quad_Subject | null; + predicate: Quad_Predicate | null; + object: Quad_Object | null; + graph: Quad_Graph | null; +} - for p in preds - if patch._allPreds.has(p.value) - return true - return false - -allPatchSubjs = (patch) -> # returns subjs as Set of strings - out = new Set() - if patch._allSubjs == undefined - patch._allSubjs = new Set() - for qq in [patch.addQuads, patch.delQuads] - for q in qq - patch._allSubjs.add(q.subject.value) - - return patch._allSubjs +class Handler { + patterns: QuadPattern[]; + innerHandlers: Handler[]; + // a function and the quad patterns it cared about + constructor(public func: ((p: Patch) => void) | null, public label: string) { + this.patterns = []; // s,p,o,g quads that should trigger the next run + this.innerHandlers = []; // Handlers requested while this one was running + } +} -class Handler - # a function and the quad patterns it cared about - constructor: (@func, @label) -> - @patterns = [] # s,p,o,g quads that should trigger the next run - @innerHandlers = [] # Handlers requested while this one was running - -class AutoDependencies - constructor: () -> - # tree of all known Handlers (at least those with non-empty - # patterns). Top node is not a handler. - @handlers = new Handler(null) - @handlerStack = [@handlers] # currently running +class AutoDependencies { + handlers: Handler; + handlerStack: Handler[]; + constructor() { + // tree of all known Handlers (at least those with non-empty + // patterns). Top node is not a handler. + this.handlers = new Handler(null, "root"); + this.handlerStack = [this.handlers]; // currently running + } - runHandler: (func, label) -> - # what if we have this func already? duplicate is safe? + runHandler(func: any, label: any) { + // what if we have this func already? duplicate is safe? - if not label? - throw new Error("missing label") + if (label == null) { + throw new Error("missing label"); + } - h = new Handler(func, label) - tailChildren = @handlerStack[@handlerStack.length - 1].innerHandlers - matchingLabel = _.filter(tailChildren, ((c) -> c.label == label)).length - # ohno, something depends on some handlers getting run twice :( - if matchingLabel < 2 - tailChildren.push(h) - #console.time("handler #{label}") - @_rerunHandler(h, null) - #console.timeEnd("handler #{label}") - #@_logHandlerTree() - - _rerunHandler: (handler, patch) -> - handler.patterns = [] - @handlerStack.push(handler) - try - handler.func(patch) - catch e - log('error running handler: ', e) - # assuming here it didn't get to do all its queries, we could - # add a *,*,*,* handler to call for sure the next time? - finally - #log('done. got: ', handler.patterns) - @handlerStack.pop() - # handler might have no watches, in which case we could forget about it + const h = new Handler(func, label); + const tailChildren = this.handlerStack[this.handlerStack.length - 1].innerHandlers; + const matchingLabel = filter(tailChildren, (c: { label: any }) => c.label === label).length; + // ohno, something depends on some handlers getting run twice :( + if (matchingLabel < 2) { + tailChildren.push(h); + } + //console.time("handler #{label}") + return this._rerunHandler(h, null); + } + //console.timeEnd("handler #{label}") + //@_logHandlerTree() - _logHandlerTree: -> - log('handler tree:') - prn = (h, depth) -> - indent = '' - for i in [0...depth] - indent += ' ' - log("#{indent} \"#{h.label}\" #{h.patterns.length} pats") - for c in h.innerHandlers - prn(c, depth + 1) - prn(@handlers, 0) - - _handlerIsAffected: (child, patchSubjs) -> - if patchSubjs == null - return true - if not child.patterns.length - return false - - for stmt in child.patterns - if stmt[0] == null # wildcard on subject - return true - if patchSubjs.has(stmt[0].value) - return true + _rerunHandler(handler: Handler, patch: any) { + handler.patterns = []; + this.handlerStack.push(handler); + try { + if (handler.func === null) { + throw new Error("tried to rerun root"); + } + return handler.func(patch); + } catch (e) { + return log("error running handler: ", e); + } finally { + // assuming here it didn't get to do all its queries, we could + // add a *,*,*,* handler to call for sure the next time? + //log('done. got: ', handler.patterns) + this.handlerStack.pop(); + } + } + // handler might have no watches, in which case we could forget about it - return false - - graphChanged: (patch) -> - # SyncedGraph is telling us this patch just got applied to the graph. - - subjs = allPatchSubjs(patch) - - rerunInners = (cur) => - toRun = cur.innerHandlers.slice() - for child in toRun - #match = @_handlerIsAffected(child, subjs) - #continue if not match - #log('match', child.label, match) - #child.innerHandlers = [] # let all children get called again - - @_rerunHandler(child, patch) - rerunInners(child) - rerunInners(@handlers) + _logHandlerTree() { + log("handler tree:"); + var prn = function (h: Handler, depth: number) { + let indent = ""; + for (let i = 0, end = depth, asc = 0 <= end; asc ? i < end : i > end; asc ? i++ : i--) { + indent += " "; + } + log(`${indent} \"${h.label}\" ${h.patterns.length} pats`); + return Array.from(h.innerHandlers).map((c: any) => prn(c, depth + 1)); + }; + return prn(this.handlers, 0); + } - askedFor: (s, p, o, g) -> - # SyncedGraph is telling us someone did a query that depended on - # quads in the given pattern. - current = @handlerStack[@handlerStack.length - 1] - if current? and current != @handlers - current.patterns.push([s, p, o, g]) - #log('push', s,p,o,g) - #else - # console.trace('read outside runHandler') + _handlerIsAffected(child: Handler, patchSubjs: Set) { + if (patchSubjs === null) { + return true; + } + if (!child.patterns.length) { + return false; + } -class window.SyncedGraph - # Main graph object for a browser to use. Syncs both ways with - # rdfdb. Meant to hide the choice of RDF lib, so we can change it - # later. - # - # Note that _applyPatch is the only method to write to the graph, so - # it can fire subscriptions. - - constructor: (@patchSenderUrl, @prefixes, @setStatus, @clearCb) -> - # patchSenderUrl is the /syncedGraph path of an rdfdb server. - # prefixes can be used in Uri(curie) calls. - @_autoDeps = new AutoDependencies() # replaces GraphWatchers - @clearGraph() + for (let stmt of Array.from(child.patterns)) { + if (stmt.subject === null) { + // wildcard on subject + return true; + } + if (patchSubjs.has(stmt.subject.value)) { + return true; + } + } - if @patchSenderUrl - @_client = new RdfDbClient(@patchSenderUrl, - @_clearGraphOnNewConnection.bind(@), - @_applyPatch.bind(@), - @setStatus) - - clearGraph: -> - # just deletes the statements; watchers are unaffected. - if @graph? - @_applyPatch({addQuads: [], delQuads: @graph.getQuads()}) + return false; + } - # if we had a Store already, this lets N3.Store free all its indices/etc - @graph = N3.Store() - @_addPrefixes(@prefixes) - @cachedFloatValues = new Map() # s + '|' + p -> number - @cachedUriValues = new Map() # s + '|' + p -> Uri + graphChanged(patch: Patch) { + // SyncedGraph is telling us this patch just got applied to the graph. + + const subjs = allPatchSubjs(patch); - _clearGraphOnNewConnection: -> # must not send a patch to the server! - log('graph: clearGraphOnNewConnection') - @clearGraph() - log('graph: clearGraphOnNewConnection done') - @clearCb() if @clearCb? - - _addPrefixes: (prefixes) -> - for k in (prefixes or {}) - @prefixes[k] = prefixes[k] - @prefixFuncs = N3.Util.prefixes(@prefixes) - - Uri: (curie) -> - if not curie? - throw new Error("no uri") - if curie.match(/^http/) - return N3.DataFactory.namedNode(curie) - part = curie.split(':') - return @prefixFuncs(part[0])(part[1]) + var rerunInners = (cur: Handler) => { + const toRun = cur.innerHandlers.slice(); + for (let child of Array.from(toRun)) { + //match = @_handlerIsAffected(child, subjs) + //continue if not match + //log('match', child.label, match) + //child.innerHandlers = [] # let all children get called again - Literal: (jsValue) -> - N3.DataFactory.literal(jsValue) - - LiteralRoundedFloat: (f) -> - N3.DataFactory.literal(d3.format(".3f")(f), - @Uri("http://www.w3.org/2001/XMLSchema#double")) + this._rerunHandler(child, patch); + rerunInners(child); + } + }; + return rerunInners(this.handlers); + } - Quad: (s, p, o, g) -> N3.DataFactory.quad(s, p, o, g) - - toJs: (literal) -> - # incomplete - parseFloat(literal.value) - - loadTrig: (trig, cb) -> # for debugging - patch = {delQuads: [], addQuads: []} - parser = N3.Parser() - parser.parse trig, (error, quad, prefixes) => - if error - throw new Error(error) - if (quad) - patch.addQuads.push(quad) - else - @_applyPatch(patch) - @_addPrefixes(prefixes) - cb() if cb - - quads: () -> # for debugging - [q.subject, q.predicate, q.object, q.graph] for q in @graph.getQuads() - - applyAndSendPatch: (patch) -> - console.time('applyAndSendPatch') - if not @_client - log('not connected-- dropping patch') - return - if !Array.isArray(patch.addQuads) || !Array.isArray(patch.delQuads) - console.timeEnd('applyAndSendPatch') - log('corrupt patch') - throw new Error("corrupt patch: #{JSON.stringify(patch)}") - - @_validatePatch(patch) + askedFor(s: Quad_Subject | null, p: Quad_Predicate | null, o: Quad_Object | null, g: Quad_Graph | null) { + // SyncedGraph is telling us someone did a query that depended on + // quads in the given pattern. + const current = this.handlerStack[this.handlerStack.length - 1]; + if (current != null && current !== this.handlers) { + return current.patterns.push({ subject: s, predicate: p, object: o, graph: g } as QuadPattern); + } + } +} - @_applyPatch(patch) - @_client.sendPatch(patch) if @_client - console.timeEnd('applyAndSendPatch') +export class SyncedGraph { + _autoDeps: AutoDependencies; + _client: any; + graph: N3.Store; + cachedFloatValues: any; + cachedUriValues: any; + prefixFuncs: (x: string) => string = (x) => x; + serial: any; + _nextNumber: any; + // Main graph object for a browser to use. Syncs both ways with + // rdfdb. Meant to hide the choice of RDF lib, so we can change it + // later. + // + // Note that _applyPatch is the only method to write to the graph, so + // it can fire subscriptions. - _validatePatch: (patch) -> - for qs in [patch.addQuads, patch.delQuads] - for q in qs - if not q.equals - throw new Error("doesn't look like a proper Quad") - if not q.subject.id or not q.graph.id? or not q.predicate.id? - throw new Error("corrupt patch: #{JSON.stringify(q)}") - - _applyPatch: (patch) -> - # In most cases you want applyAndSendPatch. - # - # This is the only method that writes to @graph! - @cachedFloatValues.clear() - @cachedUriValues.clear() - for quad in patch.delQuads - #log("remove #{JSON.stringify(quad)}") - did = @graph.removeQuad(quad) - #log("removed: #{did}") - for quad in patch.addQuads - @graph.addQuad(quad) - #log('applied patch locally', patchSizeSummary(patch)) - @_autoDeps.graphChanged(patch) + constructor( + // patchSenderUrl is the /syncedGraph path of an rdfdb server. + public patchSenderUrl: any, + // prefixes can be used in Uri(curie) calls. + public prefixes: { [short: string]: string }, + private setStatus: any, + // called if we clear the graph + private clearCb: any + ) { + this.graph = new N3.Store(); + this._autoDeps = new AutoDependencies(); // replaces GraphWatchers + this.clearGraph(); - getObjectPatch: (s, p, newObject, g) -> - # make a patch which removes existing values for (s,p,*,c) and - # adds (s,p,newObject,c). Values in other graphs are not affected. - existing = @graph.getQuads(s, p, null, g) - return { - delQuads: existing, - addQuads: [@Quad(s, p, newObject, g)] + if (this.patchSenderUrl) { + this._client = new RdfDbClient(this.patchSenderUrl, this._clearGraphOnNewConnection.bind(this), this._applyPatch.bind(this), this.setStatus); + } + } + + clearGraph() { + // just deletes the statements; watchers are unaffected. + if (this.graph != null) { + this._applyPatch({ adds: [], dels: this.graph.getQuads(null, null, null, null) }); } - patchObject: (s, p, newObject, g) -> - @applyAndSendPatch(@getObjectPatch(s, p, newObject, g)) + // if we had a Store already, this lets N3.Store free all its indices/etc + this.graph = new N3.Store(); + this._addPrefixes(this.prefixes); + this.cachedFloatValues = new Map(); // s + '|' + p -> number + return (this.cachedUriValues = new Map()); // s + '|' + p -> Uri + } + + _clearGraphOnNewConnection() { + // must not send a patch to the server! + log("graph: clearGraphOnNewConnection"); + this.clearGraph(); + log("graph: clearGraphOnNewConnection done"); + if (this.clearCb != null) { + return this.clearCb(); + } + } - clearObjects: (s, p, g) -> - @applyAndSendPatch({ - delQuads: @graph.getQuads(s, p, null, g), - addQuads: [] - }) - - runHandler: (func, label) -> - # runs your func once, tracking graph calls. if a future patch - # matches what you queried, we runHandler your func again (and - # forget your queries from the first time). + _addPrefixes(prefixes: { [x: string]: string }) { + for (let k of Array.from(prefixes || {})) { + this.prefixes[k] = prefixes[k]; + } + this.prefixFuncs = N3.Util.prefixes(this.prefixes); + } + + Uri(curie: string) { + if (curie == null) { + throw new Error("no uri"); + } + if (curie.match(/^http/)) { + return N3.DataFactory.namedNode(curie); + } + const part = curie.split(":"); + return this.prefixFuncs(part[0])(part[1]); + } - # helps with memleak? not sure yet. The point was if two matching - # labels get puushed on, we should run only one. So maybe - # appending a serial number is backwards. - @serial = 1 if not @serial - @serial += 1 - #label = label + @serial - - @_autoDeps.runHandler(func, label) + Literal(jsValue: any) { + return N3.DataFactory.literal(jsValue); + } + + LiteralRoundedFloat(f: number) { + return N3.DataFactory.literal(d3.format(".3f")(f), this.Uri("http://www.w3.org/2001/XMLSchema#double")); + } + + Quad(s: any, p: any, o: any, g: any) { + return N3.DataFactory.quad(s, p, o, g); + } + + toJs(literal: { value: any }) { + // incomplete + return parseFloat(literal.value); + } - _singleValue: (s, p) -> - @_autoDeps.askedFor(s, p, null, null) - quads = @graph.getQuads(s, p) - objs = new Set(q.object for q in quads) - - switch objs.size - when 0 - throw new Error("no value for "+s.value+" "+p.value) - when 1 - obj = objs.values().next().value - return obj - else - throw new Error("too many different values: " + JSON.stringify(quads)) + loadTrig(trig: any, cb: () => any) { + // for debugging + const patch: Patch = { dels: [], adds: [] }; + const parser = new N3.Parser(); + return parser.parse(trig, (error: any, quad: any, prefixes: any) => { + if (error) { + throw new Error(error); + } + if (quad) { + return patch.adds.push(quad); + } else { + this._applyPatch(patch); + this._addPrefixes(prefixes); + if (cb) { + return cb(); + } + } + }); + } + + quads(): any { + // for debugging + return Array.from(this.graph.getQuads(null, null, null, null)).map((q: Quad) => [q.subject, q.predicate, q.object, q.graph]); + } - floatValue: (s, p) -> - key = s.value + '|' + p.value - hit = @cachedFloatValues.get(key) - return hit if hit != undefined - #log('float miss', s, p) + applyAndSendPatch(patch: Patch) { + console.time("applyAndSendPatch"); + if (!this._client) { + log("not connected-- dropping patch"); + return; + } + if (!Array.isArray(patch.adds) || !Array.isArray(patch.dels)) { + console.timeEnd("applyAndSendPatch"); + log("corrupt patch"); + throw new Error(`corrupt patch: ${JSON.stringify(patch)}`); + } + + this._validatePatch(patch); - v = @_singleValue(s, p).value - ret = parseFloat(v) - if isNaN(ret) - throw new Error("#{s.value} #{p.value} -> #{v} not a float") - @cachedFloatValues.set(key, ret) - return ret - - stringValue: (s, p) -> - @_singleValue(s, p).value - - uriValue: (s, p) -> - key = s.value + '|' + p.value - hit = @cachedUriValues.get(key) - return hit if hit != undefined + this._applyPatch(patch); + if (this._client) { + this._client.sendPatch(patch); + } + return console.timeEnd("applyAndSendPatch"); + } + + _validatePatch(patch: Patch) { + return [patch.adds, patch.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; + })() + ); + } - ret = @_singleValue(s, p) - @cachedUriValues.set(key, ret) - return ret + _applyPatch(patch: Patch) { + // In most cases you want applyAndSendPatch. + // + // This is the only method that writes to @graph! + let quad: any; + this.cachedFloatValues.clear(); + this.cachedUriValues.clear(); + for (quad of Array.from(patch.dels)) { + //log("remove #{JSON.stringify(quad)}") + const did = this.graph.removeQuad(quad); + } + //log("removed: #{did}") + for (quad of Array.from(patch.adds)) { + this.graph.addQuad(quad); + } + //log('applied patch locally', patchSizeSummary(patch)) + return this._autoDeps.graphChanged(patch); + } - labelOrTail: (uri) -> - try - ret = @stringValue(uri, @Uri('rdfs:label')) - catch - words = uri.value.split('/') - ret = words[words.length-1] - if not ret - ret = uri.value - return ret + getObjectPatch(s: N3.NamedNode, p: N3.NamedNode, newObject: N3.Quad_Object, g: N3.NamedNode): Patch { + // make a patch which removes existing values for (s,p,*,c) and + // adds (s,p,newObject,c). Values in other graphs are not affected. + const existing = this.graph.getQuads(s, p, null, g); + return { + dels: existing, + adds: [this.Quad(s, p, newObject, g)], + }; + } + + patchObject(s: N3.NamedNode, p: N3.NamedNode, newObject: N3.Quad_Object, g: N3.NamedNode) { + return this.applyAndSendPatch(this.getObjectPatch(s, p, newObject, g)); + } - objects: (s, p) -> - @_autoDeps.askedFor(s, p, null, null) - quads = @graph.getQuads(s, p) - return (q.object for q in quads) + clearObjects(s: N3.NamedNode, p: N3.NamedNode, g: N3.NamedNode) { + return this.applyAndSendPatch({ + dels: this.graph.getQuads(s, p, null, g), + adds: [], + }); + } + + runHandler(func: any, label: any) { + // runs your func once, tracking graph calls. if a future patch + // matches what you queried, we runHandler your func again (and + // forget your queries from the first time). - subjects: (p, o) -> - @_autoDeps.askedFor(null, p, o, null) - quads = @graph.getQuads(null, p, o) - return (q.subject for q in quads) + // helps with memleak? not sure yet. The point was if two matching + // labels get puushed on, we should run only one. So maybe + // appending a serial number is backwards. + if (!this.serial) { + this.serial = 1; + } + this.serial += 1; + //label = label + @serial + + return this._autoDeps.runHandler(func, label); + } - items: (list) -> - out = [] - current = list - while true - if current == RDF + 'nil' - break - - @_autoDeps.askedFor(current, null, null, null) # a little loose + _singleValue(s: Quad_Subject, p: Quad_Predicate) { + this._autoDeps.askedFor(s, p, null, null); + const quads = this.graph.getQuads(s, p, null, null); + const objs = new Set(Array.from(quads).map((q: Quad) => q.object)); + + switch (objs.size) { + case 0: + throw new Error("no value for " + s.value + " " + p.value); + case 1: + var obj = objs.values().next().value; + return obj; + default: + throw new Error("too many different values: " + JSON.stringify(quads)); + } + } - firsts = @graph.getQuads(current, RDF + 'first', null) - rests = @graph.getQuads(current, RDF + 'rest', null) - if firsts.length != 1 - throw new Error( - "list node #{current} has #{firsts.length} rdf:first edges") - out.push(firsts[0].object) + floatValue(s: Quad_Subject, p: Quad_Predicate) { + const key = s.value + "|" + p.value; + const hit = this.cachedFloatValues.get(key); + if (hit !== undefined) { + return hit; + } + //log('float miss', s, p) + + const v = this._singleValue(s, p).value; + const ret = parseFloat(v); + if (isNaN(ret)) { + throw new Error(`${s.value} ${p.value} -> ${v} not a float`); + } + this.cachedFloatValues.set(key, ret); + return ret; + } + + stringValue(s: any, p: any) { + return this._singleValue(s, p).value; + } - if rests.length != 1 - throw new Error( - "list node #{current} has #{rests.length} rdf:rest edges") - current = rests[0].object - - return out + uriValue(s: Quad_Subject, p: Quad_Predicate) { + const key = s.value + "|" + p.value; + const hit = this.cachedUriValues.get(key); + if (hit !== undefined) { + return hit; + } + + const ret = this._singleValue(s, p); + this.cachedUriValues.set(key, ret); + return ret; + } - contains: (s, p, o) -> - @_autoDeps.askedFor(s, p, o, null) - log('contains calling getQuads when graph has ', @graph.size) - return @graph.getQuads(s, p, o).length > 0 + labelOrTail(uri: { value: { split: (arg0: string) => any } }) { + let ret: any; + try { + ret = this.stringValue(uri, this.Uri("rdfs:label")); + } catch (error) { + const words = uri.value.split("/"); + ret = words[words.length - 1]; + } + if (!ret) { + ret = uri.value; + } + return ret; + } - nextNumberedResources: (base, howMany) -> - # base is NamedNode or string - # Note this is unsafe before we're synced with the graph. It'll - # always return 'name0'. - base = base.id if base.id - results = [] + objects(s: any, p: any) { + this._autoDeps.askedFor(s, p, null, null); + const quads = this.graph.getQuads(s, p, null, null); + return Array.from(quads).map((q: { object: any }) => q.object); + } + + subjects(p: any, o: any) { + this._autoDeps.askedFor(null, p, o, null); + const quads = this.graph.getQuads(null, p, o, null); + return Array.from(quads).map((q: { subject: any }) => q.subject); + } + + items(list: any) { + const out = []; + let current = list; + while (true) { + if (current === RDF + "nil") { + break; + } + + this._autoDeps.askedFor(current, null, null, null); // a little loose - # @contains is really slow. - @_nextNumber = new Map() unless @_nextNumber? - start = @_nextNumber.get(base) - if start == undefined - start = 0 - - for serial in [start..1000] - uri = @Uri("#{base}#{serial}") - if not @contains(uri, null, null) - results.push(uri) - log('nextNumberedResources', "picked #{uri}") - @_nextNumber.set(base, serial + 1) - if results.length >= howMany - return results - throw new Error("can't make sequential uri with base #{base}") + const firsts = this.graph.getQuads(current, RDF + "first", null, null); + const rests = this.graph.getQuads(current, RDF + "rest", null, null); + if (firsts.length !== 1) { + throw new Error(`list node ${current} has ${firsts.length} rdf:first edges`); + } + out.push(firsts[0].object); + + if (rests.length !== 1) { + throw new Error(`list node ${current} has ${rests.length} rdf:rest edges`); + } + current = rests[0].object; + } + + return out; + } - nextNumberedResource: (base) -> - @nextNumberedResources(base, 1)[0] + contains(s: any, p: any, o: any) { + this._autoDeps.askedFor(s, p, o, null); + log("contains calling getQuads when graph has ", this.graph.size); + return this.graph.getQuads(s, p, o, null).length > 0; + } + + nextNumberedResources(base: { id: any }, howMany: number) { + // base is NamedNode or string + // Note this is unsafe before we're synced with the graph. It'll + // always return 'name0'. + if (base.id) { + base = base.id; + } + const results = []; + + // @contains is really slow. + if (this._nextNumber == null) { + this._nextNumber = new Map(); + } + let start = this._nextNumber.get(base); + if (start === undefined) { + start = 0; + } - contextsWithPattern: (s, p, o) -> - @_autoDeps.askedFor(s, p, o, null) - ctxs = [] - for q in @graph.getQuads(s, p, o) - ctxs.push(q.graph) - return _.unique(ctxs) + for (let serial = start, asc = start <= 1000; asc ? serial <= 1000 : serial >= 1000; asc ? serial++ : serial--) { + const uri = this.Uri(`${base}${serial}`); + if (!this.contains(uri, null, null)) { + results.push(uri); + log("nextNumberedResources", `picked ${uri}`); + this._nextNumber.set(base, serial + 1); + if (results.length >= howMany) { + return results; + } + } + } + throw new Error(`can't make sequential uri with base ${base}`); + } + + nextNumberedResource(base: any) { + return this.nextNumberedResources(base, 1)[0]; + } - sortKey: (uri) -> - parts = uri.value.split(/([0-9]+)/) - expanded = parts.map (p) -> - f = parseInt(p) - return p if isNaN(f) - return p.padStart(8, '0') - return expanded.join('') + contextsWithPattern(s: any, p: any, o: any) { + this._autoDeps.askedFor(s, p, o, null); + const ctxs = []; + for (let q of Array.from(this.graph.getQuads(s, p, o, null))) { + ctxs.push(q.graph); + } + return unique(ctxs); + } - sortedUris: (uris) -> - _.sortBy uris, @sortKey + sortKey(uri: N3.NamedNode) { + const parts = uri.value.split(/([0-9]+)/); + const expanded = parts.map(function (p: string) { + const f = parseInt(p); + if (isNaN(f)) { + return p; + } + return p.padStart(8, "0"); + }); + return expanded.join(""); + } - # temporary optimization since autodeps calls too often - @patchContainsPreds: (patch, preds) -> - patchContainsPreds(patch, preds) + sortedUris(uris: any) { + return sortBy(uris, this.sortKey); + } - prettyLiteral: (x) -> - if typeof(x) == 'number' - @LiteralRoundedFloat(x) - else - @Literal(x) + prettyLiteral(x: any) { + if (typeof x === "number") { + return this.LiteralRoundedFloat(x); + } else { + return this.Literal(x); + } + } +} diff --git a/light9/web/patch.ts b/light9/web/patch.ts new file mode 100644 --- /dev/null +++ b/light9/web/patch.ts @@ -0,0 +1,104 @@ +import debug from "debug"; +import * as async from "async"; +import { Writer, Parser, Quad, NamedNode } from "n3"; +const log = debug("patch"); + +export interface Patch { + dels: Quad[]; + adds: Quad[]; + _allPredsCache?: Set; + _allSubjsCache?: Set; +} + +interface SyncgraphPatchMessage { + patch: { adds: string; deletes: string }; +} + +export function patchSizeSummary(patch: Patch) { + return "-" + patch.dels.length + " +" + patch.adds.length; +} + +export function parseJsonPatch(input: SyncgraphPatchMessage, cb: (p: Patch) => void) { + // note response cb doesn't have an error arg. + const patch: Patch = { dels: [], adds: [] }; + + const parseAdds = (cb: () => any) => { + const parser = new Parser(); + return parser.parse(input.patch.adds, (error: any, quad: Quad, prefixes: any) => { + if (quad) { + return patch.adds.push(quad); + } else { + return cb(); + } + }); + }; + const parseDels = (cb: () => any) => { + const parser = new Parser(); + return parser.parse(input.patch.deletes, (error: any, quad: any, prefixes: any) => { + if (quad) { + return patch.dels.push(quad); + } else { + return cb(); + } + }); + }; + + return async.parallel([parseAdds, parseDels], (err: any) => cb(patch)); +} + +export function toJsonPatch(jsPatch: Patch, cb: { (json: any): any; (arg0: any): any }) { + const out: SyncgraphPatchMessage = { patch: { adds: "", deletes: "" } }; + + const writeDels = function (cb: () => any) { + const writer = new Writer({ format: "N-Quads" }); + writer.addQuads(jsPatch.dels); + return writer.end(function (err: any, result: string) { + out.patch.deletes = result; + return cb(); + }); + }; + + const writeAdds = function (cb: () => any) { + const writer = new Writer({ format: "N-Quads" }); + writer.addQuads(jsPatch.adds); + return writer.end(function (err: any, result: string) { + out.patch.adds = result; + return cb(); + }); + }; + + return async.parallel([writeDels, writeAdds], (err: any) => cb(JSON.stringify(out))); +} + +export function patchContainsPreds(patch: Patch, preds: NamedNode[]): boolean { + if (patch._allPredsCache === undefined) { + patch._allPredsCache = new Set(); + for (let qq of [patch.adds, patch.dels]) { + for (let q of Array.from(qq)) { + patch._allPredsCache.add(q.predicate.value); + } + } + } + + for (let p of Array.from(preds)) { + if (patch._allPredsCache.has(p.value)) { + return true; + } + } + return false; +} + +export function allPatchSubjs(patch: Patch): Set { + // returns subjs as Set of strings + const out = new Set(); + if (patch._allSubjsCache === undefined) { + patch._allSubjsCache = new Set(); + for (let qq of [patch.adds, patch.dels]) { + for (let q of Array.from(qq)) { + patch._allSubjsCache.add(q.subject.value); + } + } + } + + return patch._allSubjsCache; +} diff --git a/light9/web/rdfdbclient.coffee b/light9/web/rdfdbclient.ts rename from light9/web/rdfdbclient.coffee rename to light9/web/rdfdbclient.ts --- a/light9/web/rdfdbclient.coffee +++ b/light9/web/rdfdbclient.ts @@ -1,158 +1,167 @@ -log = debug('rdfdbclient') - -# for mocha -if require? - `window = {}` - `N3 = require('../../node_modules/n3/n3-browser.js')` - module.exports = window - - -toJsonPatch = (jsPatch, cb) -> - out = {patch: {adds: '', deletes: ''}} +import debug from "debug"; +import * as async from "async"; +import { parseJsonPatch, Patch, patchSizeSummary, toJsonPatch } from "./patch"; +const log = debug("rdfdbclient"); - writeDels = (cb) -> - writer = N3.Writer({ format: 'N-Quads' }) - writer.addQuads(jsPatch.delQuads) - writer.end((err, result) -> - out.patch.deletes = result - cb()) +export class RdfDbClient { + _patchesToSend: Patch[]; + _lastPingMs: number; + _patchesReceived: number; + _patchesSent: number; + _connectionId: string; + _reconnectionTimeout: number | null; + ws: WebSocket | undefined; + _pingLoopTimeout: any; + // Send and receive patches from rdfdb + // + // What this should do, and does not yet, is keep the graph + // 'coasting' over a reconnect, applying only the diffs from the old + // contents to the new ones once they're in. Then, remove all the + // clearGraph stuff in graph.coffee that doesn't even work right. + // + constructor( + public patchSenderUrl: string, + private clearGraphOnNewConnection: () => void, + private applyPatch: (p: Patch) => void, + private setStatus: (status: string) => void + ) { + this._patchesToSend = []; + this._lastPingMs = -1; + this._patchesReceived = 0; + this._patchesSent = 0; + this._connectionId = "??"; + this._reconnectionTimeout = null; + this.ws = undefined; - writeAdds = (cb) -> - writer = N3.Writer({ format: 'N-Quads' }) - writer.addQuads(jsPatch.addQuads) - writer.end((err, result) -> - out.patch.adds = result - cb()) - - async.parallel([writeDels, writeAdds], (err) -> - cb(JSON.stringify(out)) - ) - -parseJsonPatch = (input, cb) -> - # note response cb doesn't have an error arg. - patch = {delQuads: [], addQuads: []} + this._newConnection(); + } - parseAdds = (cb) => - parser = N3.Parser() - parser.parse input.patch.adds, (error, quad, prefixes) => - if (quad) - patch.addQuads.push(quad) - else - cb() - parseDels = (cb) => - parser = N3.Parser() - parser.parse input.patch.deletes, (error, quad, prefixes) => - if (quad) - patch.delQuads.push(quad) - else - cb() - - async.parallel([parseAdds, parseDels], ((err) => cb(patch))) + _updateStatus() { + const conn = (() => { + if (this.ws === undefined) { + return "no"; + } else { + switch (this.ws.readyState) { + case this.ws.CONNECTING: + return "connecting"; + case this.ws.OPEN: + return `open as ${this._connectionId}`; + case this.ws.CLOSING: + return "closing"; + case this.ws.CLOSED: + return "close"; + } + } + })(); -class window.RdfDbClient - # Send and receive patches from rdfdb - # - # What this should do, and does not yet, is keep the graph - # 'coasting' over a reconnect, applying only the diffs from the old - # contents to the new ones once they're in. Then, remove all the - # clearGraph stuff in graph.coffee that doesn't even work right. - # - constructor: (@patchSenderUrl, @clearGraphOnNewConnection, @applyPatch, - @setStatus) -> - @_patchesToSend = [] - @_lastPingMs = -1 - @_patchesReceived = 0 - @_patchesSent = 0 - @_connectionId = '??' - @_reconnectionTimeout = null - @_newConnection() + const ping = this._lastPingMs > 0 ? this._lastPingMs : "..."; + return this.setStatus(`${conn}; \ +${this._patchesReceived} recv; \ +${this._patchesSent} sent; \ +${this._patchesToSend.length} pending; \ +${ping}ms`); + } - _updateStatus: -> - ws = (if not @ws? then 'no' else switch @ws.readyState - when @ws.CONNECTING then 'connecting' - when @ws.OPEN then "open as #{@_connectionId}" - when @ws.CLOSING then 'closing' - when @ws.CLOSED then 'close' - ) + sendPatch(patch: Patch) { + log("rdfdbclient: queue patch to server ", patchSizeSummary(patch)); + this._patchesToSend.push(patch); + this._updateStatus(); + this._continueSending(); + } + + _newConnection() { + const wsOrWss = window.location.protocol.replace("http", "ws"); + const fullUrl = wsOrWss + "//" + window.location.host + this.patchSenderUrl; + if (this.ws !== undefined) { + this.ws.close(); + } + this.ws = new WebSocket(fullUrl); - ping = if @_lastPingMs > 0 then @_lastPingMs else '...' - @setStatus("#{ws}; - #{@_patchesReceived} recv; - #{@_patchesSent} sent; - #{@_patchesToSend.length} pending; - #{ping}ms") - - sendPatch: (patch) -> - log('rdfdbclient: queue patch to server ', patchSizeSummary(patch)) - @_patchesToSend.push(patch) - @_updateStatus() - @_continueSending() + this.ws.onopen = () => { + log("rdfdbclient: new connection to", fullUrl); + this._updateStatus(); + this.clearGraphOnNewConnection(); + return this._pingLoop(); + }; + + this.ws.onerror = (e: Event) => { + log("rdfdbclient: ws error " + e); + if (this.ws !== undefined) { + const closeHandler = this.ws.onclose?.bind(this.ws); + if (!closeHandler) { + throw new Error(); + } + closeHandler(new CloseEvent("forced")); + } + }; - _newConnection: -> - wsOrWss = window.location.protocol.replace('http', 'ws') - fullUrl = wsOrWss + '//' + window.location.host + @patchSenderUrl - @ws.close() if @ws? - @ws = new WebSocket(fullUrl) + this.ws.onclose = (ev: CloseEvent) => { + log("rdfdbclient: ws close"); + this._updateStatus(); + if (this._reconnectionTimeout != null) { + clearTimeout(this._reconnectionTimeout); + } + this._reconnectionTimeout = (setTimeout(this._newConnection.bind(this), 1000) as unknown) as number; + }; - @ws.onopen = => - log('rdfdbclient: new connection to', fullUrl) - @_updateStatus() - @clearGraphOnNewConnection() - @_pingLoop() + this.ws.onmessage = this._onMessage.bind(this); + } - @ws.onerror = (e) => - log('rdfdbclient: ws error ' + e) - @ws.onclose() + _pingLoop() { + if (this.ws && this.ws.readyState === this.ws.OPEN) { + this.ws.send("PING"); + this._lastPingMs = -Date.now(); - @ws.onclose = => - log('rdfdbclient: ws close') - @_updateStatus() - clearTimeout(@_reconnectionTimeout) if @_reconnectionTimeout? - @_reconnectionTimeout = setTimeout(@_newConnection.bind(@), 1000) - - @ws.onmessage = @_onMessage.bind(@) + if (this._pingLoopTimeout != null) { + clearTimeout(this._pingLoopTimeout); + } + this._pingLoopTimeout = setTimeout(this._pingLoop.bind(this), 10000); + } + } - _pingLoop: () -> - if @ws.readyState == @ws.OPEN - @ws.send('PING') - @_lastPingMs = -Date.now() - - clearTimeout(@_pingLoopTimeout) if @_pingLoopTimeout? - @_pingLoopTimeout = setTimeout(@_pingLoop.bind(@), 10000) + _onMessage(evt: { data: string }) { + const msg = evt.data; + if (msg === "PONG") { + this._lastPingMs = Date.now() + this._lastPingMs; + this._updateStatus(); + return; + } + + const input = JSON.parse(msg); + if (input.connectedAs) { + this._connectionId = input.connectedAs; + } else { + parseJsonPatch(input, this.applyPatch.bind(this)); + this._patchesReceived++; + } + return this._updateStatus(); + } - _onMessage: (evt) -> - msg = evt.data - if msg == 'PONG' - @_lastPingMs = Date.now() + @_lastPingMs - @_updateStatus() - return - - input = JSON.parse(msg) - if input.connectedAs - @_connectionId = input.connectedAs - else - parseJsonPatch(input, @applyPatch.bind(@)) - @_patchesReceived++ - @_updateStatus() + _continueSending() { + if (this.ws && this.ws.readyState !== this.ws.OPEN) { + setTimeout(this._continueSending.bind(this), 500); + return; + } + + // we could call this less often and coalesce patches together to optimize + // the dragging cases. - _continueSending: -> - if @ws.readyState != @ws.OPEN - setTimeout(@_continueSending.bind(@), 500) - return - - # we could call this less often and coalesce patches together to optimize - # the dragging cases. + const sendOne = (patch: any, cb: (arg0: any) => any) => { + return toJsonPatch(patch, (json: string) => { + log("rdfdbclient: send patch to server, " + json.length + " bytes"); + if (!this.ws) { + throw new Error("can't send"); + } + this.ws.send(json); + this._patchesSent++; + this._updateStatus(); + return cb(null); + }); + }; - sendOne = (patch, cb) => - toJsonPatch(patch, (json) => - log('rdfdbclient: send patch to server, ' + json.length + ' bytes') - @ws.send(json) - @_patchesSent++ - @_updateStatus() - cb(null) - ) - - async.eachSeries(@_patchesToSend, sendOne, () => - @_patchesToSend = [] - @_updateStatus() - ) + return async.eachSeries(this._patchesToSend, sendOne, () => { + this._patchesToSend = []; + return this._updateStatus(); + }); + } +} diff --git a/package.json b/package.json --- a/package.json +++ b/package.json @@ -9,16 +9,22 @@ "test": "test" }, "dependencies": { + "@types/async": "^3.2.13", "@types/d3": "^7.1.0", "@types/debug": "^4.1.7", + "@types/n3": "^1.10.4", "@types/node": "^17.0.31", + "@types/reconnectingwebsocket": "^1.0.7", "@types/sylvester": "^0.1.8", "@types/underscore": "^1.11.4", + "async": "^3.2.3", "d3": "^7.4.4", "debug": "^4.3.4", "knockout": "^3.5.1", "lit": "^2.2.3", + "n3": "^1.16.2", "parse-prometheus-text-format": "^1.1.1", + "reconnectingwebsocket": "^1.0.0", "sylvester": "^0.0.21", "underscore": "^1.13.3", "vite": "^2.9.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,32 +1,44 @@ lockfileVersion: 5.3 specifiers: + '@types/async': ^3.2.13 '@types/d3': ^7.1.0 '@types/debug': ^4.1.7 + '@types/n3': ^1.10.4 '@types/node': ^17.0.31 + '@types/reconnectingwebsocket': ^1.0.7 '@types/sylvester': ^0.1.8 '@types/underscore': ^1.11.4 + async: ^3.2.3 d3: ^7.4.4 debug: ^4.3.4 knockout: ^3.5.1 lit: ^2.2.3 + n3: ^1.16.2 parse-prometheus-text-format: ^1.1.1 + reconnectingwebsocket: ^1.0.0 sylvester: ^0.0.21 underscore: ^1.13.3 vite: ^2.9.1 vite-plugin-rewrite-all: ^0.1.2 dependencies: + '@types/async': 3.2.13 '@types/d3': 7.1.0 '@types/debug': 4.1.7 + '@types/n3': 1.10.4 '@types/node': 17.0.31 + '@types/reconnectingwebsocket': 1.0.7 '@types/sylvester': 0.1.8 '@types/underscore': 1.11.4 + async: 3.2.3 d3: 7.4.4 debug: 4.3.4 knockout: 3.5.1 lit: 2.2.3 + n3: 1.16.2 parse-prometheus-text-format: 1.1.1 + reconnectingwebsocket: 1.0.0 sylvester: 0.0.21 underscore: 1.13.3 vite: 2.9.1 @@ -38,6 +50,16 @@ packages: resolution: {integrity: sha512-A2e18XzPMrIh35nhIdE4uoqRzoIpEU5vZYuQN4S3Ee1zkGdYC27DP12pewbw/RLgPHzaE4kx/YqxMzebOpm0dA==} dev: false + /@rdfjs/types/1.1.0: + resolution: {integrity: sha512-5zm8bN2/CC634dTcn/0AhTRLaQRjXDZs3QfcAsQKNturHT7XVWcKy/8p3P5gXl+YkZTAmy7T5M/LyiT/jbkENw==} + dependencies: + '@types/node': 17.0.31 + dev: false + + /@types/async/3.2.13: + resolution: {integrity: sha512-7Q3awrhnvm89OzfsmqeqRQh8mh+8Pxfgq1UvSAn2nWQ5y/F3+NrbIF0RbkWq8+5dY99ozgap2b3DNBNwjLVOxw==} + dev: false + /@types/d3-array/3.0.2: resolution: {integrity: sha512-5mjGjz6XOXKOCdTajXTZ/pMsg236RdiwKPrRPWAEf/2S/+PzwY+LLYShUpeysWaMvsdS7LArh6GdUefoxpchsQ==} dev: false @@ -231,10 +253,21 @@ packages: resolution: {integrity: sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==} dev: false + /@types/n3/1.10.4: + resolution: {integrity: sha512-FfRTwcbXcScVHuAjIASveRWL6Fi6fPALl1Ge8tMESYLqU7R42LJvtdBpUi+f9YK0oQPqIN+zFFgMDFJfLMx0bg==} + dependencies: + '@types/node': 17.0.31 + rdf-js: 4.0.2 + dev: false + /@types/node/17.0.31: resolution: {integrity: sha512-AR0x5HbXGqkEx9CadRH3EBYx/VkiUgZIhP4wvPn/+5KIsgpNoyFaRlVe0Zlx9gRtg8fA06a9tskE2MSN7TcG4Q==} dev: false + /@types/reconnectingwebsocket/1.0.7: + resolution: {integrity: sha512-17pnIZsGi9P8YNp7c0ueY2WEauSxDivuFeGuMuPPJMA3qk34CnFgBHGqkxgun3HUifEuwNr8cf+9rU7vSd8i5g==} + dev: false + /@types/sylvester/0.1.8: resolution: {integrity: sha512-x1bzR4PCxvv1/9iPrbdQ15gWgP8Tp8EPjO4VLjhMijepB44BzJ/XvJavoPViSiHxlBX6NgzRgO0H+qa68lJFGA==} dev: false @@ -247,6 +280,10 @@ packages: resolution: {integrity: sha512-uO4CD2ELOjw8tasUrAhvnn2W4A0ZECOvMjCivJr4gA9pGgjv+qxKWY9GLTMVEK8ej85BxQOocUyE7hImmSQYcg==} dev: false + /async/3.2.3: + resolution: {integrity: sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g==} + dev: false + /commander/7.2.0: resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} engines: {node: '>= 10'} @@ -757,6 +794,10 @@ packages: safer-buffer: 2.1.2 dev: false + /inherits/2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + dev: false + /internmap/2.0.3: resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} engines: {node: '>=12'} @@ -797,6 +838,14 @@ packages: resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} dev: false + /n3/1.16.2: + resolution: {integrity: sha512-5vYa2HuNEJ+a26FEs4FGgfFLgaPOODaZpJlc7FS0eUjDumc4uK0cvx216PjKXBkLzmAsSqGgQPwqztcLLvwDsw==} + engines: {node: '>=8.0'} + dependencies: + queue-microtask: 1.2.3 + readable-stream: 3.6.0 + dev: false + /nanoid/3.3.2: resolution: {integrity: sha512-CuHBogktKwpm5g2sRgv83jEy2ijFzBwMoYA60orPDR7ynsLijJDqgsi4RDGj3OJpy3Ieb+LYwiRmIOGyytgITA==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -826,6 +875,29 @@ packages: source-map-js: 1.0.2 dev: false + /queue-microtask/1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + dev: false + + /rdf-js/4.0.2: + resolution: {integrity: sha512-ApvlFa/WsQh8LpPK/6hctQwG06Z9ztQQGWVtrcrf9L6+sejHNXLPOqL+w7q3hF+iL0C4sv3AX1PUtGkLNzyZ0Q==} + dependencies: + '@rdfjs/types': 1.1.0 + dev: false + + /readable-stream/3.6.0: + resolution: {integrity: sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==} + engines: {node: '>= 6'} + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + dev: false + + /reconnectingwebsocket/1.0.0: + resolution: {integrity: sha1-C4Jbq7N7ZwRFxlqn0+2XgwAgVEQ=} + dev: false + /resolve/1.22.0: resolution: {integrity: sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==} hasBin: true @@ -851,6 +923,10 @@ packages: resolution: {integrity: sha1-P4Yt+pGrdmsUiF700BEkv9oHT7Q=} dev: false + /safe-buffer/5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + dev: false + /safer-buffer/2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} dev: false @@ -864,6 +940,12 @@ packages: engines: {node: '>=0.10.0'} dev: false + /string_decoder/1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + dependencies: + safe-buffer: 5.2.1 + dev: false + /supports-preserve-symlinks-flag/1.0.0: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} @@ -878,6 +960,10 @@ packages: resolution: {integrity: sha512-QvjkYpiD+dJJraRA8+dGAU4i7aBbb2s0S3jA45TFOvg2VgqvdCDd/3N6CqA8gluk1W91GLoXg5enMUx560QzuA==} dev: false + /util-deprecate/1.0.2: + resolution: {integrity: sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=} + dev: false + /vite-plugin-rewrite-all/0.1.2_vite@2.9.1: resolution: {integrity: sha512-hBFuG043kbixgZ/ke9SzKhkO6P8a5ryxD0CmZTe+/Cz17RIKi7uSeNUJy79V4FgavZ7pWVRg0tqVwJ7lP/A2/Q==} engines: {node: '>=12.0.0'}