@spooky-sync/core 0.0.1-canary.220 → 0.0.1-canary.221

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 CHANGED
@@ -79,6 +79,20 @@ declare class RemoteDatabaseService extends AbstractDatabaseService {
79
79
  private authToken;
80
80
  constructor(config: Sp00kyConfig<any>['database'], logger: Logger$1);
81
81
  getConfig(): Sp00kyConfig<any>['database'];
82
+ /**
83
+ * Send one SurrealQL statement so that it survives the page going away.
84
+ *
85
+ * A WebSocket `send()` during `pagehide` is not guaranteed to flush — the
86
+ * browser may tear the socket down first, and the frame is simply lost.
87
+ * Measured: an unload-time release over the live socket reached the server
88
+ * zero times out of one. `fetch` with `keepalive` is the primitive the
89
+ * platform actually guarantees here, so this goes over SurrealDB's HTTP
90
+ * `/sql` endpoint instead of the RPC socket.
91
+ *
92
+ * Best-effort by design: no await, no retry, errors swallowed. Every caller
93
+ * must have a server-side fallback that makes a lost beacon a non-event.
94
+ */
95
+ beaconSql(sql: string): void;
82
96
  /**
83
97
  * Record the token every future connect should authenticate with, or `null`
84
98
  * on sign-out. See {@link authToken}.
package/dist/index.js CHANGED
@@ -1044,6 +1044,42 @@ var RemoteDatabaseService = class extends AbstractDatabaseService {
1044
1044
  return this.config;
1045
1045
  }
1046
1046
  /**
1047
+ * Send one SurrealQL statement so that it survives the page going away.
1048
+ *
1049
+ * A WebSocket `send()` during `pagehide` is not guaranteed to flush — the
1050
+ * browser may tear the socket down first, and the frame is simply lost.
1051
+ * Measured: an unload-time release over the live socket reached the server
1052
+ * zero times out of one. `fetch` with `keepalive` is the primitive the
1053
+ * platform actually guarantees here, so this goes over SurrealDB's HTTP
1054
+ * `/sql` endpoint instead of the RPC socket.
1055
+ *
1056
+ * Best-effort by design: no await, no retry, errors swallowed. Every caller
1057
+ * must have a server-side fallback that makes a lost beacon a non-event.
1058
+ */
1059
+ beaconSql(sql) {
1060
+ try {
1061
+ const { endpoint, namespace, database } = this.getConfig();
1062
+ if (!endpoint || typeof fetch !== "function") return;
1063
+ const url = new URL(endpoint);
1064
+ url.protocol = url.protocol === "wss:" ? "https:" : url.protocol === "ws:" ? "http:" : url.protocol;
1065
+ url.pathname = url.pathname.replace(/\/rpc\/?$/, "") + "/sql";
1066
+ const headers = {
1067
+ Accept: "application/json",
1068
+ "Content-Type": "text/plain"
1069
+ };
1070
+ if (namespace) headers["surreal-ns"] = namespace;
1071
+ if (database) headers["surreal-db"] = database;
1072
+ if (this.authToken) headers.Authorization = `Bearer ${this.authToken}`;
1073
+ fetch(url.toString(), {
1074
+ method: "POST",
1075
+ headers,
1076
+ body: sql,
1077
+ keepalive: true,
1078
+ credentials: "omit"
1079
+ }).catch(() => {});
1080
+ } catch {}
1081
+ }
1082
+ /**
1047
1083
  * Record the token every future connect should authenticate with, or `null`
1048
1084
  * on sign-out. See {@link authToken}.
1049
1085
  *
@@ -7246,9 +7282,9 @@ var Sp00kySync = class Sp00kySync {
7246
7282
  return;
7247
7283
  }
7248
7284
  if (ids.length === 0) return;
7249
- try {
7250
- this.remote.query("FOR $id IN $ids { LET $_released = fn::query::unsubscribe($id); };", { ids }).catch(() => {});
7251
- } catch {}
7285
+ const list = ids.map((id) => String(id)).filter((id) => /^_00_query:[0-9a-f]{64}$/.test(id)).join(", ");
7286
+ if (!list) return;
7287
+ this.remote.beaconSql(`FOR $id IN [${list}] { LET $_released = fn::query::unsubscribe($id); };`);
7252
7288
  }
7253
7289
  async registerQuery(queryHash) {
7254
7290
  this.dataModule.beginFetching(queryHash);
@@ -7699,8 +7735,8 @@ function selfAllowlistedVariant(flag, userId) {
7699
7735
 
7700
7736
  //#endregion
7701
7737
  //#region src/modules/devtools/index.ts
7702
- const CORE_VERSION = "0.0.1-canary.220";
7703
- const WASM_VERSION = "0.0.1-canary.220";
7738
+ const CORE_VERSION = "0.0.1-canary.221";
7739
+ const WASM_VERSION = "0.0.1-canary.221";
7704
7740
  const SURREAL_VERSION = "3.0.3";
7705
7741
  var DevToolsService = class DevToolsService {
7706
7742
  eventsHistory = [];
@@ -12715,7 +12751,7 @@ var Sp00kyClient = class {
12715
12751
  return new TabsCoordinator({
12716
12752
  tabId,
12717
12753
  fingerprint: computeTabsFingerprint({
12718
- coreVersion: "0.0.1-canary.220",
12754
+ coreVersion: "0.0.1-canary.221",
12719
12755
  schemaHash: hash53(this.config.schemaSurql),
12720
12756
  endpoint: this.config.database.endpoint ?? "",
12721
12757
  namespace: this.config.database.namespace,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spooky-sync/core",
3
- "version": "0.0.1-canary.220",
3
+ "version": "0.0.1-canary.221",
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.220",
64
- "@spooky-sync/ssp-wasm": "0.0.1-canary.220",
63
+ "@spooky-sync/query-builder": "0.0.1-canary.221",
64
+ "@spooky-sync/ssp-wasm": "0.0.1-canary.221",
65
65
  "@sqlite.org/sqlite-wasm": "3.53.0-build1",
66
66
  "@surrealdb/wasm": "^3.0.3",
67
67
  "blurhash": "^2.0.5",
@@ -1878,13 +1878,24 @@ export class Sp00kySync<S extends SchemaStructure> {
1878
1878
  }
1879
1879
  if (ids.length === 0) return;
1880
1880
 
1881
- try {
1882
- void this.remote
1883
- .query('FOR $id IN $ids { LET $_released = fn::query::unsubscribe($id); };', { ids })
1884
- .catch(() => {});
1885
- } catch {
1886
- // Socket already gone: the TTL sweep is the fallback, as before.
1887
- }
1881
+ // Over HTTP with `keepalive`, NOT the live socket. A WebSocket send during
1882
+ // `pagehide` is not guaranteed to flush — the browser may tear the socket
1883
+ // down first and the frame is lost. Measured on staging: releasing over the
1884
+ // socket reached the server zero times; `fetch`/`keepalive` is the only
1885
+ // primitive the platform promises to finish after the page is gone.
1886
+ //
1887
+ // Ids are inlined rather than bound because this is a bare statement, not
1888
+ // an RPC call with a params channel. They are `_00_query:<sha256>` record
1889
+ // ids the client itself derived, so the only interpolation is a hex digest.
1890
+ const list = ids
1891
+ .map((id) => String(id))
1892
+ .filter((id) => /^_00_query:[0-9a-f]{64}$/.test(id))
1893
+ .join(', ');
1894
+ if (!list) return;
1895
+
1896
+ this.remote.beaconSql(
1897
+ `FOR $id IN [${list}] { LET $_released = fn::query::unsubscribe($id); };`
1898
+ );
1888
1899
  }
1889
1900
 
1890
1901
  private async registerQuery(queryHash: string) {
@@ -96,6 +96,52 @@ export class RemoteDatabaseService extends AbstractDatabaseService {
96
96
  return this.config;
97
97
  }
98
98
 
99
+ /**
100
+ * Send one SurrealQL statement so that it survives the page going away.
101
+ *
102
+ * A WebSocket `send()` during `pagehide` is not guaranteed to flush — the
103
+ * browser may tear the socket down first, and the frame is simply lost.
104
+ * Measured: an unload-time release over the live socket reached the server
105
+ * zero times out of one. `fetch` with `keepalive` is the primitive the
106
+ * platform actually guarantees here, so this goes over SurrealDB's HTTP
107
+ * `/sql` endpoint instead of the RPC socket.
108
+ *
109
+ * Best-effort by design: no await, no retry, errors swallowed. Every caller
110
+ * must have a server-side fallback that makes a lost beacon a non-event.
111
+ */
112
+ beaconSql(sql: string): void {
113
+ try {
114
+ const { endpoint, namespace, database } = this.getConfig();
115
+ if (!endpoint || typeof fetch !== 'function') return;
116
+
117
+ // `ws(s)://host/rpc` is the socket; `http(s)://host/sql` is the same
118
+ // server's statement endpoint.
119
+ const url = new URL(endpoint);
120
+ url.protocol = url.protocol === 'wss:' ? 'https:' : url.protocol === 'ws:' ? 'http:' : url.protocol;
121
+ url.pathname = url.pathname.replace(/\/rpc\/?$/, '') + '/sql';
122
+
123
+ const headers: Record<string, string> = {
124
+ Accept: 'application/json',
125
+ 'Content-Type': 'text/plain',
126
+ };
127
+ if (namespace) headers['surreal-ns'] = namespace;
128
+ if (database) headers['surreal-db'] = database;
129
+ // Without this the statement runs unauthenticated and `$auth.id` is NONE,
130
+ // which for a per-user release means it matches nothing.
131
+ if (this.authToken) headers.Authorization = `Bearer ${this.authToken}`;
132
+
133
+ void fetch(url.toString(), {
134
+ method: 'POST',
135
+ headers,
136
+ body: sql,
137
+ keepalive: true,
138
+ credentials: 'omit',
139
+ }).catch(() => {});
140
+ } catch {
141
+ // Malformed endpoint, no fetch, blocked by CSP: the caller's fallback owns it.
142
+ }
143
+ }
144
+
99
145
  /**
100
146
  * Record the token every future connect should authenticate with, or `null`
101
147
  * on sign-out. See {@link authToken}.