@pylonsync/sync 0.8.3 → 0.9.1

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
@@ -401,6 +401,13 @@ export declare class SyncEngine {
401
401
  onBinaryFrame(handler: (bytes: Uint8Array) => void): () => void;
402
402
  /** Read the cached resolved session. Null user = anonymous. */
403
403
  resolvedSession(): ResolvedSession;
404
+ /**
405
+ * Whether `/api/auth/me` has answered during this run. Before that,
406
+ * `resolvedSession()` returns the empty placeholder, which readers must
407
+ * not mistake for a signed-out user. Notified through the same store
408
+ * channel as the session itself.
409
+ */
410
+ sessionResolved(): boolean;
404
411
  /**
405
412
  * Coarse connection state (see `SyncConnectionStatus`). Updated as
406
413
  * the WS opens/closes and reconnect attempts run; subscribers re-
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.8.3",
6
+ "version": "0.9.1",
7
7
  "type": "module",
8
8
  "main": "./src/index.ts",
9
9
  "types": "./dist/index.d.ts",
package/src/index.ts CHANGED
@@ -547,6 +547,16 @@ export class SyncEngine {
547
547
  return this.session.resolved();
548
548
  }
549
549
 
550
+ /**
551
+ * Whether `/api/auth/me` has answered during this run. Before that,
552
+ * `resolvedSession()` returns the empty placeholder, which readers must
553
+ * not mistake for a signed-out user. Notified through the same store
554
+ * channel as the session itself.
555
+ */
556
+ sessionResolved(): boolean {
557
+ return this.session.hasObserved();
558
+ }
559
+
550
560
  /**
551
561
  * Coarse connection state (see `SyncConnectionStatus`). Updated as
552
562
  * the WS opens/closes and reconnect attempts run; subscribers re-
@@ -2498,8 +2508,11 @@ export class SyncEngine {
2498
2508
  }
2499
2509
  }
2500
2510
  }
2511
+ const firstResolution = !this.session.hasObserved();
2501
2512
  this.session.commitObservation(next);
2502
- if (verdict.identityChanged) {
2513
+ if (verdict.identityChanged || firstResolution) {
2514
+ // The first answer flips `sessionResolved()` even when the
2515
+ // session itself matches the placeholder (anonymous caller).
2503
2516
  this.store.notify();
2504
2517
  }
2505
2518
  if (broadcast && this.isMultiTabLeader) {
@@ -0,0 +1,41 @@
1
+ // `sessionResolved()` tells readers whether `/api/auth/me` has answered.
2
+ // Before it does, `resolvedSession()` is the empty placeholder — a route
3
+ // guard that reads `userId == null` as "signed out" sends a returning
4
+ // user to the sign-in screen on every cold launch. The flag flips on the
5
+ // first answer, anonymous or not, and notifies store subscribers.
6
+
7
+ import { afterEach, expect, test } from "bun:test";
8
+ import { createTestEnv, type TestEnv } from "./test-harness";
9
+
10
+ let env: TestEnv | null = null;
11
+
12
+ afterEach(async () => {
13
+ if (env) {
14
+ await env.dispose();
15
+ env = null;
16
+ }
17
+ });
18
+
19
+ test("unresolved before start; resolved after /api/auth/me answers anonymous", async () => {
20
+ env = await createTestEnv({ transport: "poll" });
21
+ const { engine } = env;
22
+ expect(engine.sessionResolved()).toBe(false);
23
+ let notified = 0;
24
+ engine.store.subscribe(() => {
25
+ notified++;
26
+ });
27
+ await engine.start();
28
+ expect(engine.sessionResolved()).toBe(true);
29
+ expect(engine.resolvedSession().userId).toBeNull();
30
+ // The placeholder and the anonymous answer are identical, so only the
31
+ // resolved flag changed — subscribers must still hear about it.
32
+ expect(notified).toBeGreaterThan(0);
33
+ });
34
+
35
+ test("resolved with the signed-in identity", async () => {
36
+ env = await createTestEnv({ transport: "poll" });
37
+ env.signIn({ userId: "u1" });
38
+ await env.engine.start();
39
+ expect(env.engine.sessionResolved()).toBe(true);
40
+ expect(env.engine.resolvedSession().userId).toBe("u1");
41
+ });
@@ -104,6 +104,7 @@ export class SessionResolver {
104
104
  commitObservation(next: ResolvedSession): void {
105
105
  this.lastSeenTenant = next.tenantId;
106
106
  this._resolved = next;
107
+ this._observed = true;
107
108
  }
108
109
 
109
110
  /** Convenience for tests / migration: inspect + commit in one call.
@@ -112,7 +113,6 @@ export class SessionResolver {
112
113
  observeSession(next: ResolvedSession): SessionTransition {
113
114
  const verdict = this.inspectSession(next);
114
115
  this.commitObservation(next);
115
- this._observed = true;
116
116
  return verdict;
117
117
  }
118
118