@lemoncloud/chatic-sockets-lib 0.4.0 → 0.4.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.
@@ -82,13 +82,13 @@ export declare class AuthControllerImpl implements AuthController {
82
82
  constructor(options: AuthControllerOptions);
83
83
  get state(): AuthControllerState;
84
84
  get token(): string;
85
- /** Register token, authId, and sign callback. Idempotent; resumes when 'expired'. */
85
+ /** Register token, authId, and sign callback. Idempotent; resumes auth when inactive (after logout or expiry). */
86
86
  register: (opts: AuthRegisterOptions) => void;
87
87
  /** Resolves once authenticated (immediately if already); rejects if auth expires. */
88
88
  ready: () => Promise<void>;
89
89
  /** Switch to another site. */
90
90
  switch: (target: string, handlers?: AuthSwitchHandlers) => Promise<AuthTokenView>;
91
- /** Clear local auth state and stop the scheduler. Best-effort server notify (server-side logout is a stub). */
91
+ /** Clear local auth state and stop the scheduler. Best-effort server notify (revokes the backend session). */
92
92
  logout: () => Promise<void>;
93
93
  onAuthState: (listener: (state: AuthControllerState) => void) => (() => void);
94
94
  onTokenRefresh: (listener: (view: AuthTokenView) => void) => (() => void);
@@ -34,12 +34,12 @@ class AuthControllerImpl {
34
34
  this._token = '';
35
35
  this.authId = '';
36
36
  this.active = false;
37
- /** Register token, authId, and sign callback. Idempotent; resumes when 'expired'. */
37
+ /** Register token, authId, and sign callback. Idempotent; resumes auth when inactive (after logout or expiry). */
38
38
  this.register = (opts) => {
39
39
  this._token = opts.token;
40
40
  this.authId = opts.authId;
41
41
  this.signCallback = opts.sign;
42
- if (this._state === 'expired') {
42
+ if (!this.active) {
43
43
  this.failures = 0;
44
44
  this.active = true;
45
45
  this.setState('pending');
@@ -59,9 +59,9 @@ class AuthControllerImpl {
59
59
  off();
60
60
  resolve();
61
61
  }
62
- else if (s === 'expired') {
62
+ else if (s === 'expired' || s === '') {
63
63
  off();
64
- reject(new Error(`401 UNAUTHORIZED - auth expired`));
64
+ reject(new Error(`401 UNAUTHORIZED - auth ${s === 'expired' ? 'expired' : 'logged out'}`));
65
65
  }
66
66
  });
67
67
  });
@@ -115,14 +115,18 @@ class AuthControllerImpl {
115
115
  this.rearmRefreshTimer();
116
116
  }
117
117
  });
118
- /** Clear local auth state and stop the scheduler. Best-effort server notify (server-side logout is a stub). */
118
+ /** Clear local auth state and stop the scheduler. Best-effort server notify (revokes the backend session). */
119
119
  this.logout = () => __awaiter(this, void 0, void 0, function* () {
120
- yield this.gateway.logout({}).catch(() => undefined);
120
+ /** bump epoch so a late in-flight refresh/switch/reauth response can't resurrect auth after logout */
121
+ this.epoch++;
121
122
  this.stop();
122
123
  this._token = '';
123
124
  this.authId = '';
124
125
  this.signCallback = undefined;
125
126
  this.setState('');
127
+ if (this.options.client.state === 'connected') {
128
+ yield this.gateway.logout({}).catch(() => undefined);
129
+ }
126
130
  });
127
131
  this.onAuthState = (listener) => {
128
132
  this.stateListeners.add(listener);
@@ -24,6 +24,7 @@ export interface ChannelSyncPlanOptions<TView extends SyncableView = SyncableVie
24
24
  export declare class ChannelSyncPlan<TView extends SyncableView = SyncableView> implements DomainSyncPlan<ChannelSyncTarget> {
25
25
  private readonly options;
26
26
  readonly domain = "channel";
27
+ readonly requiresAuth = true;
27
28
  readonly idleBackoff: SyncBackoffOptions;
28
29
  constructor(options?: ChannelSyncPlanOptions<TView>);
29
30
  supports: (target: SyncTargetDescriptor) => target is ChannelSyncTarget;
@@ -20,6 +20,7 @@ class ChannelSyncPlan {
20
20
  var _a;
21
21
  this.options = options;
22
22
  this.domain = 'channel';
23
+ this.requiresAuth = true;
23
24
  this.supports = (target) => (target === null || target === void 0 ? void 0 : target.type) === 'channel';
24
25
  this.getKey = (target) => { var _a; return `channel:${(_a = target === null || target === void 0 ? void 0 : target.id) !== null && _a !== void 0 ? _a : ''}`; };
25
26
  this.getIntervalMs = (target) => { var _a, _b; return (_b = (_a = target.intervalMs) !== null && _a !== void 0 ? _a : this.options.intervalMs) !== null && _b !== void 0 ? _b : 2000; };
@@ -41,6 +41,7 @@ export interface ChatSyncPlanOptions<TMessage extends ChatMessageLike = ChatMess
41
41
  export declare class ChatSyncPlan<TMessage extends ChatMessageLike = ChatMessageLike> implements DomainSyncPlan<ChatSyncTarget> {
42
42
  private readonly options;
43
43
  readonly domain = "chat";
44
+ readonly requiresAuth = true;
44
45
  readonly idleBackoff: SyncBackoffOptions;
45
46
  private readonly chains;
46
47
  constructor(options?: ChatSyncPlanOptions<TMessage>);
@@ -22,6 +22,7 @@ class ChatSyncPlan {
22
22
  var _a;
23
23
  this.options = options;
24
24
  this.domain = 'chat';
25
+ this.requiresAuth = true;
25
26
  this.chains = new Map();
26
27
  this.supports = (target) => (target === null || target === void 0 ? void 0 : target.type) === 'chat';
27
28
  this.getKey = (target) => { var _a; return `chat:${(_a = target === null || target === void 0 ? void 0 : target.id) !== null && _a !== void 0 ? _a : ''}`; };
@@ -21,6 +21,7 @@ export interface DeviceSyncPlanOptions {
21
21
  export declare class DeviceSyncPlan implements DomainSyncPlan<DeviceSyncTarget> {
22
22
  private readonly options;
23
23
  readonly domain = "device";
24
+ readonly requiresAuth = false;
24
25
  readonly failurePolicy: SyncFailurePolicy;
25
26
  readonly idleBackoff: SyncBackoffOptions;
26
27
  constructor(options?: DeviceSyncPlanOptions);
@@ -15,6 +15,7 @@ class DeviceSyncPlan {
15
15
  var _a, _b;
16
16
  this.options = options;
17
17
  this.domain = 'device';
18
+ this.requiresAuth = false;
18
19
  this.supports = (target) => (target === null || target === void 0 ? void 0 : target.type) === 'device';
19
20
  this.getKey = (target) => { var _a; return `device:${(_a = target === null || target === void 0 ? void 0 : target.id) !== null && _a !== void 0 ? _a : 'current'}`; };
20
21
  this.getIntervalMs = (target) => { var _a, _b; return (_b = (_a = target.intervalMs) !== null && _a !== void 0 ? _a : this.options.intervalMs) !== null && _b !== void 0 ? _b : 2000; };
@@ -24,6 +24,7 @@ export interface JoinSyncPlanOptions<TView extends SyncableView = SyncableView>
24
24
  export declare class JoinSyncPlan<TView extends SyncableView = SyncableView> implements DomainSyncPlan<JoinSyncTarget> {
25
25
  private readonly options;
26
26
  readonly domain = "join";
27
+ readonly requiresAuth = true;
27
28
  readonly idleBackoff: SyncBackoffOptions;
28
29
  constructor(options?: JoinSyncPlanOptions<TView>);
29
30
  supports: (target: SyncTargetDescriptor) => target is JoinSyncTarget;
@@ -20,6 +20,7 @@ class JoinSyncPlan {
20
20
  var _a;
21
21
  this.options = options;
22
22
  this.domain = 'join';
23
+ this.requiresAuth = true;
23
24
  this.supports = (target) => (target === null || target === void 0 ? void 0 : target.type) === 'join';
24
25
  this.getKey = (target) => { var _a; return `join:${(_a = target === null || target === void 0 ? void 0 : target.id) !== null && _a !== void 0 ? _a : ''}`; };
25
26
  // Default poll interval (10s); the scheduler backs this off x2 up to 60s while idle.
@@ -24,6 +24,7 @@ export interface PlaceSyncPlanOptions<TView extends SyncableView = SyncableView>
24
24
  export declare class PlaceSyncPlan<TView extends SyncableView = SyncableView> implements DomainSyncPlan<PlaceSyncTarget> {
25
25
  private readonly options;
26
26
  readonly domain = "place";
27
+ readonly requiresAuth = true;
27
28
  readonly idleBackoff: SyncBackoffOptions;
28
29
  constructor(options?: PlaceSyncPlanOptions<TView>);
29
30
  supports: (target: SyncTargetDescriptor) => target is PlaceSyncTarget;
@@ -20,6 +20,7 @@ class PlaceSyncPlan {
20
20
  var _a;
21
21
  this.options = options;
22
22
  this.domain = 'place';
23
+ this.requiresAuth = true;
23
24
  this.supports = (target) => (target === null || target === void 0 ? void 0 : target.type) === 'place';
24
25
  this.getKey = (target) => { var _a; return `place:${(_a = target === null || target === void 0 ? void 0 : target.id) !== null && _a !== void 0 ? _a : ''}`; };
25
26
  this.getIntervalMs = (target) => { var _a, _b; return (_b = (_a = target.intervalMs) !== null && _a !== void 0 ? _a : this.options.intervalMs) !== null && _b !== void 0 ? _b : 2000; };
@@ -24,6 +24,7 @@ export interface ProfileSyncPlanOptions<TView extends SyncableView = SyncableVie
24
24
  export declare class ProfileSyncPlan<TView extends SyncableView = SyncableView> implements DomainSyncPlan<ProfileSyncTarget> {
25
25
  private readonly options;
26
26
  readonly domain = "profile";
27
+ readonly requiresAuth = true;
27
28
  readonly idleBackoff: SyncBackoffOptions;
28
29
  constructor(options?: ProfileSyncPlanOptions<TView>);
29
30
  supports: (target: SyncTargetDescriptor) => target is ProfileSyncTarget;
@@ -20,6 +20,7 @@ class ProfileSyncPlan {
20
20
  var _a;
21
21
  this.options = options;
22
22
  this.domain = 'profile';
23
+ this.requiresAuth = true;
23
24
  this.supports = (target) => (target === null || target === void 0 ? void 0 : target.type) === 'profile';
24
25
  this.getKey = (target) => { var _a; return `profile:${(_a = target === null || target === void 0 ? void 0 : target.id) !== null && _a !== void 0 ? _a : ''}`; };
25
26
  this.getIntervalMs = (target) => { var _a, _b; return (_b = (_a = target.intervalMs) !== null && _a !== void 0 ? _a : this.options.intervalMs) !== null && _b !== void 0 ? _b : 2000; };
@@ -27,6 +27,7 @@ export declare class DomainSyncScheduler implements SyncScheduler {
27
27
  private readonly random;
28
28
  private readonly unsubs;
29
29
  constructor(options: SyncSchedulerOptions);
30
+ private isEntryAuthReady;
30
31
  start: (target: SyncTargetDescriptor) => void;
31
32
  stop: (target: SyncTargetDescriptor) => void;
32
33
  stopAll: () => void;
@@ -36,7 +37,11 @@ export declare class DomainSyncScheduler implements SyncScheduler {
36
37
  private resolvePlan;
37
38
  private findEntry;
38
39
  private buildContext;
40
+ /** run the connect-time hook (may poll the server, e.g. chat catch-up) then fresh-start the poll loop. */
41
+ private activateEntry;
39
42
  private handleConnected;
43
+ private resumeAuthGated;
44
+ private pauseAuthGated;
40
45
  private stopAllTimers;
41
46
  private clearTimer;
42
47
  private scheduleNow;
@@ -24,6 +24,12 @@ class DomainSyncScheduler {
24
24
  this.targets = new Map();
25
25
  this.snapshots = new Map();
26
26
  this.unsubs = [];
27
+ this.isEntryAuthReady = (entry) => {
28
+ if (!entry.plan.requiresAuth)
29
+ return true;
30
+ const auth = this.options.client.auth;
31
+ return !!auth && auth.state === 'authenticated';
32
+ };
27
33
  this.start = (target) => {
28
34
  const plan = this.resolvePlan(target);
29
35
  const key = plan.getKey(target);
@@ -32,8 +38,9 @@ class DomainSyncScheduler {
32
38
  entry.target = Object.assign(Object.assign({}, entry.target), target);
33
39
  return;
34
40
  }
35
- this.targets.set(key, { target, plan });
36
- if (this.options.client.state === 'connected')
41
+ const created = { target, plan };
42
+ this.targets.set(key, created);
43
+ if (this.options.client.state === 'connected' && this.isEntryAuthReady(created))
37
44
  this.scheduleNow(key);
38
45
  };
39
46
  this.stop = (target) => {
@@ -104,18 +111,40 @@ class DomainSyncScheduler {
104
111
  }),
105
112
  stop: target => this.stop(target),
106
113
  });
107
- this.handleConnected = () => __awaiter(this, void 0, void 0, function* () {
114
+ /** run the connect-time hook (may poll the server, e.g. chat catch-up) then fresh-start the poll loop. */
115
+ this.activateEntry = (key, entry, ctx) => __awaiter(this, void 0, void 0, function* () {
108
116
  var _k, _l;
117
+ yield ((_l = (_k = entry.plan).onConnected) === null || _l === void 0 ? void 0 : _l.call(_k, entry.target, ctx));
118
+ entry.failures = 0;
119
+ entry.goneStreak = 0;
120
+ entry.idleStreak = 0;
121
+ this.scheduleNow(key);
122
+ });
123
+ this.handleConnected = () => __awaiter(this, void 0, void 0, function* () {
109
124
  const ctx = this.buildContext();
110
125
  for (const [key, entry] of this.targets.entries()) {
111
- // reconnect = fresh start
112
- entry.failures = 0;
113
- entry.goneStreak = 0;
114
- entry.idleStreak = 0;
115
- yield ((_l = (_k = entry.plan).onConnected) === null || _l === void 0 ? void 0 : _l.call(_k, entry.target, ctx));
116
- this.scheduleNow(key);
126
+ // gated (requiresAuth) entries activate on `authenticated` instead — see resumeAuthGated.
127
+ // onConnected itself may poll the server (chat catch-up), so it must be gated too, not just run().
128
+ if (!this.isEntryAuthReady(entry))
129
+ continue;
130
+ yield this.activateEntry(key, entry, ctx);
117
131
  }
118
132
  });
133
+ this.resumeAuthGated = () => __awaiter(this, void 0, void 0, function* () {
134
+ if (this.options.client.state !== 'connected')
135
+ return;
136
+ const ctx = this.buildContext();
137
+ for (const [key, entry] of this.targets.entries()) {
138
+ if (entry.plan.requiresAuth)
139
+ yield this.activateEntry(key, entry, ctx);
140
+ }
141
+ });
142
+ this.pauseAuthGated = () => {
143
+ this.targets.forEach(entry => {
144
+ if (entry.plan.requiresAuth)
145
+ this.clearTimer(entry);
146
+ });
147
+ };
119
148
  this.stopAllTimers = () => {
120
149
  this.targets.forEach(entry => this.clearTimer(entry));
121
150
  };
@@ -143,7 +172,7 @@ class DomainSyncScheduler {
143
172
  if (!entry)
144
173
  return;
145
174
  this.clearTimer(entry);
146
- if (this.options.client.state !== 'connected')
175
+ if (this.options.client.state !== 'connected' || !this.isEntryAuthReady(entry))
147
176
  return;
148
177
  const baseMs = (_d = (_a = entry.target.intervalMs) !== null && _a !== void 0 ? _a : (_c = (_b = entry.plan).getIntervalMs) === null || _c === void 0 ? void 0 : _c.call(_b, entry.target)) !== null && _d !== void 0 ? _d : 5000;
149
178
  const intervalMs = this.computeInterval(entry, baseMs);
@@ -170,7 +199,7 @@ class DomainSyncScheduler {
170
199
  this.toTimerKey = (key) => `${this.timerPrefix}${key}`;
171
200
  this.runEntry = (key) => __awaiter(this, void 0, void 0, function* () {
172
201
  const entry = this.targets.get(key);
173
- if (!entry || this.options.client.state !== 'connected')
202
+ if (!entry || this.options.client.state !== 'connected' || !this.isEntryAuthReady(entry))
174
203
  return;
175
204
  if (entry.inFlight)
176
205
  return;
@@ -236,6 +265,8 @@ class DomainSyncScheduler {
236
265
  return;
237
266
  if (!type.startsWith(`${entry.target.type}.`))
238
267
  return;
268
+ if (!this.isEntryAuthReady(entry))
269
+ return;
239
270
  // nudge resets idle backoff (best-effort)
240
271
  entry.idleStreak = 0;
241
272
  yield entry.plan.onTrigger(entry.target, message, ctx).catch(() => undefined);
@@ -261,6 +292,15 @@ class DomainSyncScheduler {
261
292
  this.unsubs.push(options.client.onMessage(({ message }) => {
262
293
  void this.handleTrigger(message);
263
294
  }));
295
+ const auth = options.client.auth;
296
+ if (auth) {
297
+ this.unsubs.push(auth.onAuthState(state => {
298
+ if (state === 'authenticated')
299
+ void this.resumeAuthGated();
300
+ else
301
+ this.pauseAuthGated();
302
+ }));
303
+ }
264
304
  }
265
305
  }
266
306
  exports.DomainSyncScheduler = DomainSyncScheduler;
@@ -166,6 +166,7 @@ export interface DomainSyncContext {
166
166
  }
167
167
  export interface DomainSyncPlan<TTarget extends SyncTargetDescriptor = SyncTargetDescriptor> {
168
168
  readonly domain: string;
169
+ readonly requiresAuth?: boolean;
169
170
  readonly failurePolicy?: SyncFailurePolicy;
170
171
  readonly idleBackoff?: SyncBackoffOptions;
171
172
  supports(target: SyncTargetDescriptor): target is TTarget;
@@ -52,3 +52,14 @@ export interface AuthTokenView {
52
52
  /** cloud id */
53
53
  cloudId?: string;
54
54
  }
55
+ /** client-safe view of the logout result (curated from backend `UserLogoutResult`). */
56
+ export interface AuthLogoutView {
57
+ /** logged-out user id */
58
+ id?: string;
59
+ /** revoked auth-id */
60
+ authId?: string;
61
+ /** revoke flag (1 when revoked) */
62
+ revoked?: number;
63
+ /** revoke completion time (epoch ms) */
64
+ revokedAt?: number;
65
+ }
@@ -32,6 +32,8 @@ export interface AuthSwitchRequestData extends AuthRefreshRequestData {
32
32
  export interface AuthLogoutRequestData {
33
33
  /** (internal) auth-id */
34
34
  authId?: string;
35
+ /** (internal) userAgent the signature */
36
+ userAgent?: string;
35
37
  }
36
38
  declare module '../types' {
37
39
  interface SocketPacketRegistry {
@@ -52,7 +54,7 @@ declare module '../types' {
52
54
  };
53
55
  'auth.logout': {
54
56
  request: AuthLogoutRequestData;
55
- response: never;
57
+ response: unknown;
56
58
  error: null;
57
59
  };
58
60
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lemoncloud/chatic-sockets-lib",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "Client websocket transport and sync runtime for chatic sockets v2",
5
5
  "main": "dist/client-socket-v2/index.js",
6
6
  "types": "dist/client-socket-v2/index.d.ts",