@spooky-sync/core 0.0.1-canary.227 → 0.0.1-canary.229
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.d.ts +22 -0
- package/dist/index.js +49 -3
- package/package.json +3 -3
- package/src/sp00ky.auth-order.test.ts +20 -0
- package/src/sp00ky.ts +52 -0
package/dist/index.d.ts
CHANGED
|
@@ -2587,6 +2587,28 @@ declare class Sp00kyClient<S extends SchemaStructure> {
|
|
|
2587
2587
|
* outbox - and the connection supervisor keeps retrying underneath.
|
|
2588
2588
|
*/
|
|
2589
2589
|
private initRemote;
|
|
2590
|
+
/**
|
|
2591
|
+
* Write the signed-in user's own row, as the SERVER just returned it, into
|
|
2592
|
+
* the local store.
|
|
2593
|
+
*
|
|
2594
|
+
* `AuthService.check()` (boot verification, sign-in, an app-driven refresh)
|
|
2595
|
+
* runs `SELECT * FROM ONLY $auth.id` and keeps the answer on
|
|
2596
|
+
* `auth.currentUser`; nothing consumed it beyond the id. Meanwhile the app's
|
|
2597
|
+
* own `user` query paints local-first from whatever body the store holds,
|
|
2598
|
+
* and that body is only refreshed when the server's row version reaches
|
|
2599
|
+
* this client through membership (`_00_list_ref`). While the SSP is down or
|
|
2600
|
+
* bootstrapping that never happens, so a device that registered before the
|
|
2601
|
+
* account was verified kept rendering a row without `email_verified` and
|
|
2602
|
+
* the app parked the user on "Verify your email" over a server that said
|
|
2603
|
+
* "already verified". The authoritative row was already in hand.
|
|
2604
|
+
*
|
|
2605
|
+
* MERGE semantics (`CacheModule.saveBatch`) keep local-only fields, and
|
|
2606
|
+
* re-using the memoized version keeps the sync dedup honest: this is a
|
|
2607
|
+
* field refresh, not a version claim. `notifyTableQueries` re-materializes
|
|
2608
|
+
* the `user` queries so the gate flips without waiting for a stream update.
|
|
2609
|
+
* Best-effort: a failure here leaves things exactly as they were.
|
|
2610
|
+
*/
|
|
2611
|
+
private persistVerifiedUser;
|
|
2590
2612
|
private bucketSwitchChain;
|
|
2591
2613
|
private pendingBucketTarget;
|
|
2592
2614
|
/**
|
package/dist/index.js
CHANGED
|
@@ -7764,8 +7764,8 @@ function selfAllowlistedVariant(flag, userId) {
|
|
|
7764
7764
|
|
|
7765
7765
|
//#endregion
|
|
7766
7766
|
//#region src/modules/devtools/index.ts
|
|
7767
|
-
const CORE_VERSION = "0.0.1-canary.
|
|
7768
|
-
const WASM_VERSION = "0.0.1-canary.
|
|
7767
|
+
const CORE_VERSION = "0.0.1-canary.229";
|
|
7768
|
+
const WASM_VERSION = "0.0.1-canary.229";
|
|
7769
7769
|
const SURREAL_VERSION = "3.0.3";
|
|
7770
7770
|
var DevToolsService = class DevToolsService {
|
|
7771
7771
|
eventsHistory = [];
|
|
@@ -12780,7 +12780,7 @@ var Sp00kyClient = class {
|
|
|
12780
12780
|
return new TabsCoordinator({
|
|
12781
12781
|
tabId,
|
|
12782
12782
|
fingerprint: computeTabsFingerprint({
|
|
12783
|
-
coreVersion: "0.0.1-canary.
|
|
12783
|
+
coreVersion: "0.0.1-canary.229",
|
|
12784
12784
|
schemaHash: hash53(this.config.schemaSurql),
|
|
12785
12785
|
endpoint: this.config.database.endpoint ?? "",
|
|
12786
12786
|
namespace: this.config.database.namespace,
|
|
@@ -12971,6 +12971,7 @@ var Sp00kyClient = class {
|
|
|
12971
12971
|
Category: "sp00ky-client::Sp00kyClient::authChange"
|
|
12972
12972
|
}, "sync.setCurrentUserId failed");
|
|
12973
12973
|
}
|
|
12974
|
+
await this.persistVerifiedUser();
|
|
12974
12975
|
});
|
|
12975
12976
|
await this.sync.init();
|
|
12976
12977
|
this.logger.debug({ Category: "sp00ky-client::Sp00kyClient::init" }, "Sync initialized");
|
|
@@ -13027,6 +13028,51 @@ var Sp00kyClient = class {
|
|
|
13027
13028
|
}, "Auth verification failed; keeping the restored session");
|
|
13028
13029
|
}
|
|
13029
13030
|
}
|
|
13031
|
+
/**
|
|
13032
|
+
* Write the signed-in user's own row, as the SERVER just returned it, into
|
|
13033
|
+
* the local store.
|
|
13034
|
+
*
|
|
13035
|
+
* `AuthService.check()` (boot verification, sign-in, an app-driven refresh)
|
|
13036
|
+
* runs `SELECT * FROM ONLY $auth.id` and keeps the answer on
|
|
13037
|
+
* `auth.currentUser`; nothing consumed it beyond the id. Meanwhile the app's
|
|
13038
|
+
* own `user` query paints local-first from whatever body the store holds,
|
|
13039
|
+
* and that body is only refreshed when the server's row version reaches
|
|
13040
|
+
* this client through membership (`_00_list_ref`). While the SSP is down or
|
|
13041
|
+
* bootstrapping that never happens, so a device that registered before the
|
|
13042
|
+
* account was verified kept rendering a row without `email_verified` and
|
|
13043
|
+
* the app parked the user on "Verify your email" over a server that said
|
|
13044
|
+
* "already verified". The authoritative row was already in hand.
|
|
13045
|
+
*
|
|
13046
|
+
* MERGE semantics (`CacheModule.saveBatch`) keep local-only fields, and
|
|
13047
|
+
* re-using the memoized version keeps the sync dedup honest: this is a
|
|
13048
|
+
* field refresh, not a version claim. `notifyTableQueries` re-materializes
|
|
13049
|
+
* the `user` queries so the gate flips without waiting for a stream update.
|
|
13050
|
+
* Best-effort: a failure here leaves things exactly as they were.
|
|
13051
|
+
*/
|
|
13052
|
+
async persistVerifiedUser() {
|
|
13053
|
+
const row = this.auth.currentUser;
|
|
13054
|
+
if (!row || !(row.id instanceof RecordId) || Object.keys(row).length <= 1) return;
|
|
13055
|
+
const rid = row.id;
|
|
13056
|
+
const table = rid.table.toString();
|
|
13057
|
+
const tableSchema = this.config.schema.tables.find((t) => t.name === table);
|
|
13058
|
+
if (!tableSchema) return;
|
|
13059
|
+
try {
|
|
13060
|
+
const encoded = encodeRecordId(rid);
|
|
13061
|
+
const version = this.cache.lookup(encoded) || 1;
|
|
13062
|
+
await this.cache.saveBatch([{
|
|
13063
|
+
table,
|
|
13064
|
+
op: "UPDATE",
|
|
13065
|
+
record: cleanRecord(tableSchema.columns, row),
|
|
13066
|
+
version
|
|
13067
|
+
}]);
|
|
13068
|
+
await this.dataModule.notifyTableQueries(table);
|
|
13069
|
+
} catch (e) {
|
|
13070
|
+
this.logger.warn({
|
|
13071
|
+
err: e,
|
|
13072
|
+
Category: "sp00ky-client::Sp00kyClient::persistVerifiedUser"
|
|
13073
|
+
}, "Could not persist the verified user row locally");
|
|
13074
|
+
}
|
|
13075
|
+
}
|
|
13030
13076
|
bucketSwitchChain = Promise.resolve();
|
|
13031
13077
|
pendingBucketTarget = null;
|
|
13032
13078
|
/**
|
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.229",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -60,8 +60,8 @@
|
|
|
60
60
|
}
|
|
61
61
|
},
|
|
62
62
|
"dependencies": {
|
|
63
|
-
"@spooky-sync/query-builder": "0.0.1-canary.
|
|
64
|
-
"@spooky-sync/ssp-wasm": "0.0.1-canary.
|
|
63
|
+
"@spooky-sync/query-builder": "0.0.1-canary.229",
|
|
64
|
+
"@spooky-sync/ssp-wasm": "0.0.1-canary.229",
|
|
65
65
|
"@sqlite.org/sqlite-wasm": "3.53.0-build1",
|
|
66
66
|
"@surrealdb/wasm": "^3.0.3",
|
|
67
67
|
"blurhash": "^2.0.5",
|
|
@@ -63,6 +63,26 @@ describe('Sp00kyClient.auth.subscribe ordering invariant', () => {
|
|
|
63
63
|
}
|
|
64
64
|
});
|
|
65
65
|
|
|
66
|
+
it('persists the server-verified user row only after the bucket switch', () => {
|
|
67
|
+
// `persistVerifiedUser` writes `auth.currentUser` (the row `check()` got
|
|
68
|
+
// from `SELECT * FROM ONLY $auth.id`) into the local store so the app's
|
|
69
|
+
// `user` query stops rendering a stale, pre-verification body. It must run
|
|
70
|
+
// AFTER `ensureLocalBucket`, or the row lands in the previous user's
|
|
71
|
+
// bucket on a sign-in that switches accounts.
|
|
72
|
+
const match = source.match(
|
|
73
|
+
/this\.auth\.subscribe\(\s*async\s*\(\s*userId\s*[^)]*\)\s*=>\s*\{([\s\S]*?)\n {6}\}\s*\)/
|
|
74
|
+
);
|
|
75
|
+
expect(match).not.toBeNull();
|
|
76
|
+
const stripped = match![1]
|
|
77
|
+
.split('\n')
|
|
78
|
+
.map((line) => line.replace(/\/\/.*$/, ''))
|
|
79
|
+
.join('\n');
|
|
80
|
+
const bucketIdx = stripped.indexOf('this.ensureLocalBucket(userId)');
|
|
81
|
+
const persistIdx = stripped.indexOf('this.persistVerifiedUser()');
|
|
82
|
+
expect(persistIdx, 'persistVerifiedUser() must be called from the auth subscriber').toBeGreaterThanOrEqual(0);
|
|
83
|
+
expect(persistIdx).toBeGreaterThan(bucketIdx);
|
|
84
|
+
});
|
|
85
|
+
|
|
66
86
|
it('writes the boot-bucket hint synchronously and switches buckets before sync.setCurrentUserId', () => {
|
|
67
87
|
const match = source.match(
|
|
68
88
|
/this\.auth\.subscribe\(\s*async\s*\(\s*userId\s*[^)]*\)\s*=>\s*\{([\s\S]*?)\n {6}\}\s*\)/
|
package/src/sp00ky.ts
CHANGED
|
@@ -53,6 +53,8 @@ import type { AppReleaseOptions } from './modules/app-release/index';
|
|
|
53
53
|
import { LocalStoragePersistenceClient } from './services/persistence/localstorage';
|
|
54
54
|
import { ANON_USER_ID, bucketIdForUser } from './modules/ref-tables';
|
|
55
55
|
import { parseQueryParams, encodeRecordId, parseDuration } from './utils/index';
|
|
56
|
+
import { cleanRecord } from './utils/parser';
|
|
57
|
+
import { RecordId } from 'surrealdb';
|
|
56
58
|
import { SurrealDBPersistenceClient } from './services/persistence/surrealdb';
|
|
57
59
|
import { ResilientPersistenceClient } from './services/persistence/resilient';
|
|
58
60
|
import { detectSharedTabsSupport } from './services/tabs/support';
|
|
@@ -976,6 +978,8 @@ export class Sp00kyClient<S extends SchemaStructure> {
|
|
|
976
978
|
'sync.setCurrentUserId failed'
|
|
977
979
|
);
|
|
978
980
|
}
|
|
981
|
+
// After the bucket switch, so the row lands in THIS user's store.
|
|
982
|
+
await this.persistVerifiedUser();
|
|
979
983
|
});
|
|
980
984
|
|
|
981
985
|
await this.sync.init();
|
|
@@ -1083,6 +1087,54 @@ export class Sp00kyClient<S extends SchemaStructure> {
|
|
|
1083
1087
|
}
|
|
1084
1088
|
}
|
|
1085
1089
|
|
|
1090
|
+
/**
|
|
1091
|
+
* Write the signed-in user's own row, as the SERVER just returned it, into
|
|
1092
|
+
* the local store.
|
|
1093
|
+
*
|
|
1094
|
+
* `AuthService.check()` (boot verification, sign-in, an app-driven refresh)
|
|
1095
|
+
* runs `SELECT * FROM ONLY $auth.id` and keeps the answer on
|
|
1096
|
+
* `auth.currentUser`; nothing consumed it beyond the id. Meanwhile the app's
|
|
1097
|
+
* own `user` query paints local-first from whatever body the store holds,
|
|
1098
|
+
* and that body is only refreshed when the server's row version reaches
|
|
1099
|
+
* this client through membership (`_00_list_ref`). While the SSP is down or
|
|
1100
|
+
* bootstrapping that never happens, so a device that registered before the
|
|
1101
|
+
* account was verified kept rendering a row without `email_verified` and
|
|
1102
|
+
* the app parked the user on "Verify your email" over a server that said
|
|
1103
|
+
* "already verified". The authoritative row was already in hand.
|
|
1104
|
+
*
|
|
1105
|
+
* MERGE semantics (`CacheModule.saveBatch`) keep local-only fields, and
|
|
1106
|
+
* re-using the memoized version keeps the sync dedup honest: this is a
|
|
1107
|
+
* field refresh, not a version claim. `notifyTableQueries` re-materializes
|
|
1108
|
+
* the `user` queries so the gate flips without waiting for a stream update.
|
|
1109
|
+
* Best-effort: a failure here leaves things exactly as they were.
|
|
1110
|
+
*/
|
|
1111
|
+
private async persistVerifiedUser(): Promise<void> {
|
|
1112
|
+
const row = this.auth.currentUser as Record<string, unknown> | null;
|
|
1113
|
+
if (!row || !(row.id instanceof RecordId) || Object.keys(row).length <= 1) return;
|
|
1114
|
+
const rid = row.id as RecordId;
|
|
1115
|
+
const table = rid.table.toString();
|
|
1116
|
+
const tableSchema = this.config.schema.tables.find((t) => t.name === table);
|
|
1117
|
+
if (!tableSchema) return;
|
|
1118
|
+
try {
|
|
1119
|
+
const encoded = encodeRecordId(rid);
|
|
1120
|
+
const version = this.cache.lookup(encoded) || 1;
|
|
1121
|
+
await this.cache.saveBatch([
|
|
1122
|
+
{
|
|
1123
|
+
table,
|
|
1124
|
+
op: 'UPDATE',
|
|
1125
|
+
record: cleanRecord(tableSchema.columns, row) as RecordWithId,
|
|
1126
|
+
version,
|
|
1127
|
+
},
|
|
1128
|
+
]);
|
|
1129
|
+
await this.dataModule.notifyTableQueries(table);
|
|
1130
|
+
} catch (e) {
|
|
1131
|
+
this.logger.warn(
|
|
1132
|
+
{ err: e, Category: 'sp00ky-client::Sp00kyClient::persistVerifiedUser' },
|
|
1133
|
+
'Could not persist the verified user row locally'
|
|
1134
|
+
);
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1086
1138
|
// Serializes bucket switches from rapid auth flips; `pendingBucketTarget`
|
|
1087
1139
|
// makes intermediate targets collapse (A→anon→B never opens the anon bucket).
|
|
1088
1140
|
private bucketSwitchChain: Promise<void> = Promise.resolve();
|