@spooky-sync/core 0.0.1-canary.241 → 0.0.1-canary.243
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/dist/index.js
CHANGED
|
@@ -1073,8 +1073,8 @@ function selfAllowlistedVariant(flag, userId) {
|
|
|
1073
1073
|
|
|
1074
1074
|
//#endregion
|
|
1075
1075
|
//#region src/modules/devtools/index.ts
|
|
1076
|
-
const CORE_VERSION = "0.0.1-canary.
|
|
1077
|
-
const WASM_VERSION = "0.0.1-canary.
|
|
1076
|
+
const CORE_VERSION = "0.0.1-canary.243";
|
|
1077
|
+
const WASM_VERSION = "0.0.1-canary.243";
|
|
1078
1078
|
const SURREAL_VERSION = "3.0.3";
|
|
1079
1079
|
var DevToolsService = class DevToolsService {
|
|
1080
1080
|
eventsHistory = [];
|
|
@@ -12473,7 +12473,7 @@ function createTabsCoordinator(config, s, host) {
|
|
|
12473
12473
|
return new TabsCoordinator({
|
|
12474
12474
|
tabId: s.tabId,
|
|
12475
12475
|
fingerprint: computeTabsFingerprint({
|
|
12476
|
-
coreVersion: "0.0.1-canary.
|
|
12476
|
+
coreVersion: "0.0.1-canary.243",
|
|
12477
12477
|
schemaHash: hash53(config.schemaSurql),
|
|
12478
12478
|
endpoint: config.database.endpoint ?? "",
|
|
12479
12479
|
namespace: config.database.namespace,
|
package/dist/sqlite-open.js
CHANGED
|
@@ -205,6 +205,15 @@ function reviveRow(json) {
|
|
|
205
205
|
* than as text. `COALESCE` covers a row sharing none of the requested keys,
|
|
206
206
|
* where the subquery yields NULL and `reviveRow` would throw.
|
|
207
207
|
*
|
|
208
|
+
* The one value `je.value` does NOT carry back is a TOP-LEVEL boolean. SQLite
|
|
209
|
+
* has no boolean type, so `json_each` hands `true`/`false` out as the integers
|
|
210
|
+
* 1/0 and `json_group_object` writes integers back. A projected row therefore
|
|
211
|
+
* read `published: 1` where the same row read whole read `published: true`,
|
|
212
|
+
* and every `=== true` in an app turned false the moment a screen painted from
|
|
213
|
+
* the local cache. (A boolean NESTED in an object or array survives: those come
|
|
214
|
+
* back as whole JSON documents, untouched.) `je.type` still knows what it was,
|
|
215
|
+
* so the two boolean types are rebuilt as JSON literals on the way out.
|
|
216
|
+
*
|
|
208
217
|
* Deliberately unaliased, so the emitted statement keeps the exact shape the
|
|
209
218
|
* callers already produce (`FROM "t" WHERE id IN (…)`).
|
|
210
219
|
*
|
|
@@ -214,7 +223,7 @@ function reviveRow(json) {
|
|
|
214
223
|
function projectedDataSql(fields, bind) {
|
|
215
224
|
const keys = ["id", ...fields];
|
|
216
225
|
for (const k of keys) bind.push(k);
|
|
217
|
-
return `COALESCE((SELECT json_group_object(je.key, je.value) FROM json_each(data) je WHERE je.key IN (${keys.map(() => "?").join(", ")})), '{}') AS data`;
|
|
226
|
+
return `COALESCE((SELECT json_group_object(je.key, CASE je.type WHEN 'true' THEN json('true') WHEN 'false' THEN json('false') ELSE je.value END) FROM json_each(data) je WHERE je.key IN (${keys.map(() => "?").join(", ")})), '{}') AS data`;
|
|
218
227
|
}
|
|
219
228
|
function project(row, fields) {
|
|
220
229
|
const out = {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spooky-sync/core",
|
|
3
|
-
"version": "0.0.1-canary.
|
|
3
|
+
"version": "0.0.1-canary.243",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -63,8 +63,8 @@
|
|
|
63
63
|
}
|
|
64
64
|
},
|
|
65
65
|
"dependencies": {
|
|
66
|
-
"@spooky-sync/query-builder": "0.0.1-canary.
|
|
67
|
-
"@spooky-sync/ssp-wasm": "0.0.1-canary.
|
|
66
|
+
"@spooky-sync/query-builder": "0.0.1-canary.243",
|
|
67
|
+
"@spooky-sync/ssp-wasm": "0.0.1-canary.243",
|
|
68
68
|
"@sqlite.org/sqlite-wasm": "3.53.0-build1",
|
|
69
69
|
"@surrealdb/wasm": "^3.0.3",
|
|
70
70
|
"blurhash": "^2.0.5",
|
|
@@ -99,6 +99,15 @@ export function reviveRow(json: string): Row {
|
|
|
99
99
|
* than as text. `COALESCE` covers a row sharing none of the requested keys,
|
|
100
100
|
* where the subquery yields NULL and `reviveRow` would throw.
|
|
101
101
|
*
|
|
102
|
+
* The one value `je.value` does NOT carry back is a TOP-LEVEL boolean. SQLite
|
|
103
|
+
* has no boolean type, so `json_each` hands `true`/`false` out as the integers
|
|
104
|
+
* 1/0 and `json_group_object` writes integers back. A projected row therefore
|
|
105
|
+
* read `published: 1` where the same row read whole read `published: true`,
|
|
106
|
+
* and every `=== true` in an app turned false the moment a screen painted from
|
|
107
|
+
* the local cache. (A boolean NESTED in an object or array survives: those come
|
|
108
|
+
* back as whole JSON documents, untouched.) `je.type` still knows what it was,
|
|
109
|
+
* so the two boolean types are rebuilt as JSON literals on the way out.
|
|
110
|
+
*
|
|
102
111
|
* Deliberately unaliased, so the emitted statement keeps the exact shape the
|
|
103
112
|
* callers already produce (`FROM "t" WHERE id IN (…)`).
|
|
104
113
|
*
|
|
@@ -109,8 +118,14 @@ export function projectedDataSql(fields: string[], bind: unknown[]): string {
|
|
|
109
118
|
const keys = ['id', ...fields];
|
|
110
119
|
for (const k of keys) bind.push(k);
|
|
111
120
|
const placeholders = keys.map(() => '?').join(', ');
|
|
121
|
+
// `json('true')` / `json('false')` return JSON-subtyped values, so
|
|
122
|
+
// `json_group_object` embeds them as the literals `true` / `false` rather
|
|
123
|
+
// than as quoted text.
|
|
124
|
+
const value =
|
|
125
|
+
`CASE je.type WHEN 'true' THEN json('true') ` +
|
|
126
|
+
`WHEN 'false' THEN json('false') ELSE je.value END`;
|
|
112
127
|
return (
|
|
113
|
-
`COALESCE((SELECT json_group_object(je.key,
|
|
128
|
+
`COALESCE((SELECT json_group_object(je.key, ${value}) ` +
|
|
114
129
|
`FROM json_each(data) je WHERE je.key IN (${placeholders})), '{}') AS data`
|
|
115
130
|
);
|
|
116
131
|
}
|
|
@@ -12,7 +12,9 @@ import type { Row } from './cache-engine';
|
|
|
12
12
|
* over it, which is what both engine paths did before. The traps it has to
|
|
13
13
|
* clear are an ABSENT key (must stay absent, not become an explicit null — the
|
|
14
14
|
* difference `json_object`/`json_extract` would have introduced), a STORED
|
|
15
|
-
* null, nested objects/arrays,
|
|
15
|
+
* null, nested objects/arrays, the `{__u8}` tag that carries binary, and a
|
|
16
|
+
* TOP-LEVEL boolean (SQLite has no boolean type, so an unguarded projection
|
|
17
|
+
* hands `true` back as the integer 1).
|
|
16
18
|
*/
|
|
17
19
|
|
|
18
20
|
function makeDb(rows: Row[]) {
|
|
@@ -49,9 +51,10 @@ const FIXTURE: Row[] = [
|
|
|
49
51
|
{ id: 'game:2', white: null, pgn: 'heavy', sort_index: -3 }, // STORED null
|
|
50
52
|
{ id: 'game:3', pgn: 'heavy', sort_index: -4 }, // 'white' ABSENT
|
|
51
53
|
{ id: 'game:4', white: 'w', pgn: 'heavy', sort_index: -1, meta: { a: 1, b: [2, 3] } },
|
|
54
|
+
{ id: 'game:5', white: 'w', pgn: 'heavy', sort_index: 0, rated: true, draw: false },
|
|
52
55
|
];
|
|
53
|
-
const IDS = ['game:1', 'game:2', 'game:3', 'game:4'];
|
|
54
|
-
const FIELDS = ['white', 'sort_index', 'meta'];
|
|
56
|
+
const IDS = ['game:1', 'game:2', 'game:3', 'game:4', 'game:5'];
|
|
57
|
+
const FIELDS = ['white', 'sort_index', 'meta', 'rated', 'draw'];
|
|
55
58
|
|
|
56
59
|
describe('projectedDataSql against real SQLite', () => {
|
|
57
60
|
it('is byte-identical to parse-then-project', () => {
|
|
@@ -91,6 +94,25 @@ describe('projectedDataSql against real SQLite', () => {
|
|
|
91
94
|
for (const row of projectedRows(db, IDS, ['sort_index'])) expect(row.id).toBeTruthy();
|
|
92
95
|
});
|
|
93
96
|
|
|
97
|
+
it('round-trips a top-level boolean as a boolean, not as 1/0', () => {
|
|
98
|
+
// The bug this guards: `json_each` yields SQLite integers for JSON booleans,
|
|
99
|
+
// so a projected row read `rated: 1` and every `row.rated === true` in a
|
|
100
|
+
// consuming app went false as soon as the screen painted from the cache.
|
|
101
|
+
// WhitePawn's /explore detail page read exactly that as "this collection was
|
|
102
|
+
// unpublished or deleted by its author".
|
|
103
|
+
const db = makeDb(FIXTURE);
|
|
104
|
+
const row = projectedRows(db, IDS, FIELDS).find((r) => r.id === 'game:5')!;
|
|
105
|
+
expect(row.rated).toBe(true);
|
|
106
|
+
expect(row.draw).toBe(false);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('leaves a boolean nested in an object or array alone', () => {
|
|
110
|
+
const db = makeDb([{ id: 'game:7', flags: { on: true, off: false }, list: [true, false] }]);
|
|
111
|
+
const [row] = projectedRows(db, ['game:7'], ['flags', 'list']);
|
|
112
|
+
expect(row!.flags).toEqual({ on: true, off: false });
|
|
113
|
+
expect(row!.list).toEqual([true, false]);
|
|
114
|
+
});
|
|
115
|
+
|
|
94
116
|
it('yields an empty object, not NULL, for a row sharing none of the keys', () => {
|
|
95
117
|
// Without COALESCE the subquery returns NULL here and reviveRow throws.
|
|
96
118
|
const db = makeDb([{ id: 'game:8', pgn: 'heavy' }]);
|