@yuneta/gobj-js 7.13.8 → 7.16.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.
@@ -5100,6 +5100,234 @@ function build_path(...segments) {
5100
5100
  }
5101
5101
  return result;
5102
5102
  }
5103
+ /***************************************
5104
+ * Flat json (id -> value)
5105
+ ***************************************/
5106
+ /************************************************************
5107
+ * THE FLAT FORM, AND WHY IT IS SHAPED LIKE THIS
5108
+ *
5109
+ * A json seen as a table: one row per LEAF, the id being the
5110
+ * PATH of the item and the value its value. Easier to store,
5111
+ * to compare and to diff than the nested form, and the only
5112
+ * one a person can read when two configurations disagree.
5113
+ *
5114
+ * {a: {b: 1}, c: [10, 20]}
5115
+ * -> {"a`b": 1, "c`[0]": 10, "c`[1]": 20}
5116
+ *
5117
+ * Same grammar as the C side (kwid.c), because the point is
5118
+ * that a flat json written by either can be read by the other:
5119
+ *
5120
+ * - segments joined by '`', the SDK's own path delimiter;
5121
+ * - a literal '`' in a key is DOUBLED, so no key is
5122
+ * forbidden -- a dict keyed by "1630" is a dict, not an
5123
+ * array of 1631 elements, which is what the first C
5124
+ * implementation answered;
5125
+ * - an array index is '[N]', canonical, no leading zeros;
5126
+ * - a dict key starting with '[' doubles it ('[[0]'), so it
5127
+ * can never be read as an index;
5128
+ * - AN EMPTY CONTAINER IS A LEAF: {} and [] hold no leaves,
5129
+ * and 'properties': {} is everywhere in our configs.
5130
+ *
5131
+ * flat2json() THROWS instead of guessing: an id used as leaf and
5132
+ * as container would depend on the order the ids are read in.
5133
+ ************************************************************/
5134
+ var FLAT_MAX_DEPTH = 256;
5135
+ var FLAT_MAX_INDEX = 1e5;
5136
+ /************************************************************
5137
+ * Is this raw segment an array index, '[N]'?
5138
+ * Returns the index, or null.
5139
+ ************************************************************/
5140
+ function flat_seg_index(seg) {
5141
+ if (typeof seg !== "string" || seg.length < 3 || seg[0] !== "[") return null;
5142
+ if (seg[seg.length - 1] !== "]") return null;
5143
+ const body = seg.slice(1, -1);
5144
+ if (!/^[0-9]+$/.test(body)) return null;
5145
+ if (body.length > 1 && body[0] === "0") return null;
5146
+ return parseInt(body, 10);
5147
+ }
5148
+ /************************************************************
5149
+ * Escape one dict key.
5150
+ ************************************************************/
5151
+ function flat_escape_key(key) {
5152
+ const s = String(key).replace(/`/g, "``");
5153
+ return s[0] === "[" ? "[" + s : s;
5154
+ }
5155
+ /************************************************************
5156
+ * Compose a flat id from its segments.
5157
+ *
5158
+ * A STRING segment is a dict key and gets escaped; a NUMBER is
5159
+ * an array index and is written '[N]'. They are two types and
5160
+ * not two spellings: as strings, the key "[0]" and the index 0
5161
+ * would both be "[0]", which is the ambiguity '[N]' removes.
5162
+ ************************************************************/
5163
+ function flat_key_join(segments) {
5164
+ if (!Array.isArray(segments) || segments.length === 0) return "";
5165
+ return segments.map((seg) => typeof seg === "number" ? `[${seg}]` : flat_escape_key(seg)).join("`");
5166
+ }
5167
+ /************************************************************
5168
+ * Split a flat id into its segments, undoing the escapes.
5169
+ *
5170
+ * An index comes back as a NUMBER and a dict key as a STRING.
5171
+ ************************************************************/
5172
+ function flat_key_split(key) {
5173
+ if (typeof key !== "string") return null;
5174
+ let raw = [];
5175
+ let cur = "";
5176
+ for (let i = 0; i < key.length; i++) {
5177
+ if (key[i] === "`") {
5178
+ if (key[i + 1] === "`") {
5179
+ cur += "`";
5180
+ i++;
5181
+ continue;
5182
+ }
5183
+ raw.push(cur);
5184
+ cur = "";
5185
+ continue;
5186
+ }
5187
+ cur += key[i];
5188
+ }
5189
+ raw.push(cur);
5190
+ return raw.map((seg) => {
5191
+ const idx = flat_seg_index(seg);
5192
+ if (idx !== null) return idx;
5193
+ if (seg[0] === "[" && seg[1] === "[") return seg.slice(1);
5194
+ return seg;
5195
+ });
5196
+ }
5197
+ /************************************************************
5198
+ * Nested -> flat.
5199
+ ************************************************************/
5200
+ function json2flat(jn) {
5201
+ const flat = {};
5202
+ const walk = (value, path, depth) => {
5203
+ if (depth > FLAT_MAX_DEPTH) throw new Error(`json2flat: deeper than ${FLAT_MAX_DEPTH}`);
5204
+ if (value !== null && typeof value === "object" && !Array.isArray(value)) {
5205
+ const keys = Object.keys(value);
5206
+ if (keys.length > 0) {
5207
+ for (const key of keys) {
5208
+ const seg = flat_escape_key(key);
5209
+ walk(value[key], depth === 0 ? seg : path + "`" + seg, depth + 1);
5210
+ }
5211
+ return;
5212
+ }
5213
+ } else if (Array.isArray(value) && value.length > 0) {
5214
+ value.forEach((child, index) => {
5215
+ const seg = `[${index}]`;
5216
+ walk(child, depth === 0 ? seg : path + "`" + seg, depth + 1);
5217
+ });
5218
+ return;
5219
+ }
5220
+ if (depth === 0) return;
5221
+ flat[path] = value;
5222
+ };
5223
+ if (jn === void 0 || jn === null) return flat;
5224
+ walk(jn, "", 0);
5225
+ return flat;
5226
+ }
5227
+ /************************************************************
5228
+ * Flat -> nested. Throws on the FIRST id that does not fit:
5229
+ * rebuilding "most of it" is how a configuration comes back
5230
+ * subtly different from the one that was saved.
5231
+ ************************************************************/
5232
+ function flat2json(flat) {
5233
+ if (flat === null || typeof flat !== "object" || Array.isArray(flat)) throw new Error("flat2json: a flat json is an object of id -> value");
5234
+ let root = null;
5235
+ for (const key of Object.keys(flat)) {
5236
+ const segments = flat_key_split(key);
5237
+ if (!segments || segments.length === 0) throw new Error(`flat2json: malformed flat id: '${key}'`);
5238
+ if (segments.length > FLAT_MAX_DEPTH) throw new Error(`flat2json: '${key}' deeper than ${FLAT_MAX_DEPTH}`);
5239
+ const first_is_index = typeof segments[0] === "number";
5240
+ if (root === null) root = first_is_index ? [] : {};
5241
+ else if (first_is_index !== Array.isArray(root)) throw new Error(`flat2json: '${key}' does not fit a ${Array.isArray(root) ? "array" : "object"} root`);
5242
+ let current = root;
5243
+ for (let i = 0; i < segments.length; i++) {
5244
+ const seg = segments[i];
5245
+ const is_index = typeof seg === "number";
5246
+ const is_last = i === segments.length - 1;
5247
+ const next_is_index = is_last ? false : typeof segments[i + 1] === "number";
5248
+ if (is_index && seg > FLAT_MAX_INDEX) throw new Error(`flat2json: array index ${seg} over the limit (${FLAT_MAX_INDEX}) in '${key}'`);
5249
+ if (is_index !== Array.isArray(current)) throw new Error(`flat2json: '${key}' uses a ${is_index ? "array index" : "key"} where the tree already has a ${Array.isArray(current) ? "array" : "object"}`);
5250
+ if (is_last) {
5251
+ if (Array.isArray(current)) {
5252
+ while (current.length <= seg) current.push(null);
5253
+ current[seg] = flat[key];
5254
+ } else {
5255
+ if (Object.prototype.hasOwnProperty.call(current, seg)) throw new Error(`flat2json: '${key}' is written twice, or once as a value and once as a container`);
5256
+ current[seg] = flat[key];
5257
+ }
5258
+ break;
5259
+ }
5260
+ let child;
5261
+ if (Array.isArray(current)) {
5262
+ while (current.length <= seg) current.push(null);
5263
+ child = current[seg];
5264
+ if (child === null || child === void 0) {
5265
+ child = next_is_index ? [] : {};
5266
+ current[seg] = child;
5267
+ }
5268
+ } else {
5269
+ child = current[seg];
5270
+ if (child === void 0) {
5271
+ child = next_is_index ? [] : {};
5272
+ current[seg] = child;
5273
+ }
5274
+ }
5275
+ if (child === null || typeof child !== "object") throw new Error(`flat2json: '${key}' walks through a value: the same id is a leaf and a container at once`);
5276
+ if (next_is_index !== Array.isArray(child)) throw new Error(`flat2json: '${key}' needs a ${next_is_index ? "array" : "object"} where the tree already has a ${Array.isArray(child) ? "array" : "object"}`);
5277
+ current = child;
5278
+ }
5279
+ }
5280
+ return root === null ? {} : root;
5281
+ }
5282
+ /************************************************************
5283
+ * Deep equality, for comparing two leaf values.
5284
+ ************************************************************/
5285
+ function flat_value_equal(a, b) {
5286
+ if (a === b) return true;
5287
+ if (a === null || b === null || typeof a !== "object" || typeof b !== "object") return false;
5288
+ return JSON.stringify(a) === JSON.stringify(b);
5289
+ }
5290
+ /************************************************************
5291
+ * What changed between two flat dicts:
5292
+ *
5293
+ * {added: {id: value}, removed: {id: value},
5294
+ * changed: {id: {from, to}}}
5295
+ ************************************************************/
5296
+ function flat_diff(flat1, flat2) {
5297
+ const a = flat1 && typeof flat1 === "object" ? flat1 : {};
5298
+ const b = flat2 && typeof flat2 === "object" ? flat2 : {};
5299
+ const diff = {
5300
+ added: {},
5301
+ removed: {},
5302
+ changed: {}
5303
+ };
5304
+ for (const key of Object.keys(a)) if (!Object.prototype.hasOwnProperty.call(b, key)) diff.removed[key] = a[key];
5305
+ else if (!flat_value_equal(a[key], b[key])) diff.changed[key] = {
5306
+ from: a[key],
5307
+ to: b[key]
5308
+ };
5309
+ for (const key of Object.keys(b)) if (!Object.prototype.hasOwnProperty.call(a, key)) diff.added[key] = b[key];
5310
+ return diff;
5311
+ }
5312
+ /************************************************************
5313
+ * Apply that diff to a flat dict, which is MUTATED.
5314
+ *
5315
+ * On the flat form on purpose: there an id addresses one value,
5316
+ * so applying is setting and deleting, with nothing to walk and
5317
+ * nothing to guess. Nested is json2flat -> flat_apply -> flat2json.
5318
+ ************************************************************/
5319
+ function flat_apply(flat, diff) {
5320
+ if (flat === null || typeof flat !== "object" || Array.isArray(flat)) throw new Error("flat_apply: a flat json is an object of id -> value");
5321
+ if (diff === null || typeof diff !== "object") throw new Error("flat_apply: a diff is the object flat_diff() answers");
5322
+ for (const key of Object.keys(diff.removed || {})) delete flat[key];
5323
+ for (const key of Object.keys(diff.added || {})) flat[key] = diff.added[key];
5324
+ for (const key of Object.keys(diff.changed || {})) {
5325
+ const change = diff.changed[key];
5326
+ if (!change || !Object.prototype.hasOwnProperty.call(change, "to")) throw new Error(`flat_apply: changed '${key}' without a 'to'`);
5327
+ flat[key] = change.to;
5328
+ }
5329
+ return flat;
5330
+ }
5103
5331
  //#endregion
5104
5332
  //#region src/dbsimple.js
5105
5333
  /*********************************************************************************
@@ -6682,6 +6910,11 @@ exports.duplicate_objects = duplicate_objects;
6682
6910
  exports.empty_string = empty_string;
6683
6911
  exports.escapeHtml = escapeHtml;
6684
6912
  exports.event_flag_t = event_flag_t;
6913
+ exports.flat2json = flat2json;
6914
+ exports.flat_apply = flat_apply;
6915
+ exports.flat_diff = flat_diff;
6916
+ exports.flat_key_join = flat_key_join;
6917
+ exports.flat_key_split = flat_key_split;
6685
6918
  exports.gclass_add_ev_action = gclass_add_ev_action;
6686
6919
  exports.gclass_add_event_type = gclass_add_event_type;
6687
6920
  exports.gclass_add_state = gclass_add_state;
@@ -6809,6 +7042,7 @@ exports.jdb_get_by_idx = jdb_get_by_idx;
6809
7042
  exports.jdb_get_topic = jdb_get_topic;
6810
7043
  exports.jdb_init = jdb_init;
6811
7044
  exports.jdb_update = jdb_update;
7045
+ exports.json2flat = json2flat;
6812
7046
  exports.json_array_append = json_array_append;
6813
7047
  exports.json_array_append_new = json_array_append_new;
6814
7048
  exports.json_array_extend = json_array_extend;