@graphy/core.class.scribable
Version:
Serialize RDF fast
319 lines (250 loc) • 7.19 kB
JavaScript
// queueMicrotask shim
{
// not defined or not a function
if('function' !== typeof queueMicrotask) {
// create resolved promise
let dp_resolve = Promise.resolve();
// try to redefine
try {
// eslint-disable-next-line no-global-assign
queueMicrotask = fk => dp_resolve.then(fk)
.catch(e_callback => setTimeout(() => {
throw e_callback;
}, 0));
}
// oh well, at least we tried
catch(e_define) {}
}
}
const factory = require('@graphy/core.data.factory');
const stream = require('@graphy/core.iso.stream');
// max string buffer size
const N_DEFAULT_MAX_BUFFER = 1 << 15; // 32 KiB
class Scribable extends stream.Transform {
// flush buffer
static _flush_buffer(k_self) {
// no buffer; exit
if(!k_self._s_push) return;
// push buffer to stream
k_self.push(k_self._s_push);
// reset buffer
k_self._s_push = '';
}
constructor(gc_scribable={}) {
super({
writableObjectMode: true,
readableObjectMode: false,
});
let {
prefixes: h_prefixes={},
} = gc_scribable;
// internal buffer
this._s_push = '';
// max buffer length
this._n_max_buffer = gc_scribable.max_buffer || gc_scribable.maxBuffer || N_DEFAULT_MAX_BUFFER;
// prefixes
this._h_prefixes = factory.cache_prefixes(h_prefixes || {});
// on new source(s)
this.on('pipe', (ds_src) => {
// listen for prefix events
ds_src.on('prefix', (s_prefix_id, p_iri) => {
this.write({
type: 'prefixes',
value: {
[s_prefix_id]: p_iri,
},
});
});
// listen for comment events
ds_src.on('comment', (s_comment) => {
this.write({
type: 'comment',
value: s_comment,
});
});
});
// bind event listeners
if(gc_scribable.close) this.once('close', gc_scribable.close);
if(gc_scribable.drain) this.on('drain', gc_scribable.drain);
if(gc_scribable.error) this.on('error', gc_scribable.error);
if(gc_scribable.finish) this.once('finish', gc_scribable.finish);
if(gc_scribable.data) this.on('data', gc_scribable.data);
if(gc_scribable.end) this.once('end', gc_scribable.end);
if(gc_scribable.warning) this.on('warning', gc_scribable.warning);
}
_serialize_hash_comment(s_comment) {
return '# '+s_comment.replace(/\n/g, '\n# ')+'\n';
}
_serialize_newlines(n_newlines=1) {
return '\n'.repeat(n_newlines);
}
_serialize_c4r(hc4r_quads) {
let h_prefixes = this._h_prefixes;
let a_unions = [];
let s_write = '';
// each graph in quads hash
for(let sc1_graph in hc4r_quads) {
// non-default graph; union from dataset
if('*' !== sc1_graph) a_unions.push(sc1_graph);
// add all quads from graph
s_write += this._serialize_c3r(hc4r_quads[sc1_graph]);
}
// a union was performed
if(a_unions.length) {
// warn about implicit union
let s_warning = `Destination format does not support quads; an implicit union into the default graph was performed on the quads contained in graphs: ${a_unions.map(sc1 => factory.c1(sc1, h_prefixes).verbose()).join(', ')}`;
// emit warning, wasn't listened to; force thru warn/stderr channel
if(!this.emit('warning', s_warning)) {
console.warn(s_warning);
}
}
return s_write;
}
_serialize_c3() {
throw new Error(`Write event type 'c3' not supported by ${this.constructor.name}`);
}
_serialize_c4() {
throw new Error(`Write event type 'c4' not supported by ${this.constructor.name}`);
}
_serialize_c3r() {
throw new Error(`Write event type 'c3r' should have been implemented by subclass ${this.constructor.name}`);
}
_serialize_quad() {
throw new Error(`Write event type 'quad' should have been implemented by subclass ${this.constructor.name}`);
}
// ignorable events
_serialize_comment() {} // eslint-disable-line class-methods-use-this
// update prefix mappings
_update_prefixes(h_prefixes_in, b_terse=false) {
// merge prefixes
let h_prefixes = {
...this._h_prefixes,
...h_prefixes_in,
};
// recache prefixes
this._h_prefixes = factory.cachePrefixes(h_prefixes || {}, b_terse);
}
// implement stream.Transform
_transform(g_event, s_encoding, fke_transform) {
let w_write;
// try to serialize input value
try {
w_write = this.serialize(g_event);
}
// serialization error
catch(e_serialize) {
// report error
fke_transform(e_serialize);
// bail on transform
return e_serialize;
}
// data to push
if(w_write) {
// flush internal buffer
Scribable._flush_buffer(this);
// push data to stream
this.push(w_write);
}
// nothing returned from serialization
else {
let nl_push = this._s_push.length;
// internal buffer high water mark
if(nl_push > this._n_max_buffer) {
Scribable._flush_buffer(this);
}
// allow buffer to build
else if(nl_push) {
// do not worry about clearing timeouts
queueMicrotask(() => Scribable._flush_buffer(this));
}
}
// callback
fke_transform();
}
// queue data to be pushed later
_queue(s_push) {
this._s_push += s_push;
// internal buffer high water mark
if(this._s_push.length > this._n_max_buffer) {
Scribable._flush_buffer(this);
}
else {
// do not worry about clearing timeouts
queueMicrotask(() => Scribable._flush_buffer(this));
}
}
// route event object to serialization method
serialize(g_event) {
switch(g_event.type) {
// rdfjs quad
// eslint-disable-next-line no-undefined
case undefined: return this._serialize_quad(g_event);
// concise triple struct strict-mode
case 'c3r': return this._serialize_c3r(g_event.value);
// concise quad struct strict-mode
case 'c4r': return this._serialize_c4r(g_event.value);
// array of events
case 'array': {
// string building
let s_write = '';
// each subevent
for(let g_sub of g_event.value) {
// build serialization string
let s_push = this.serialize(g_sub);
// something to push
if(s_push) {
s_write += s_push;
}
// push was queued
else if(this._s_push) {
// concat to write
s_write += this._s_push;
// reset push
this._s_push = '';
}
}
// all done
return s_write;
}
// quad
case 'quad': return this._serialize_quad(g_event.value);
// concise triple struct
case 'c3': return this._serialize_c3(g_event.value);
// concise quad struct
case 'c4': return this._serialize_c4(g_event.value);
// prefixes
case 'prefixes': {
return this._serialize_prefixes(g_event.value);
}
// comment
case 'comment': {
return this._serialize_comment(g_event.value);
}
// newline(s)
case 'newline':
case 'newlines': {
return this._serialize_newlines(g_event.value);
}
// no such event type
default: {
throw new Error(`no such writable data event type for RDF stream: '${g_event.type}'`);
}
}
}
// rinse off buffer to writable
rinse() {
this._reset();
Scribable._flush_buffer(this);
}
_flush() {
// flush buffer
Scribable._flush_buffer(this);
// eof
this.push(null);
}
}
Object.assign(Scribable.prototype, {
isGraphyWritable: true,
_serialize_prefixes: Scribable.prototype._update_prefixes,
});
module.exports = Scribable;