@saolabs/client 1.5.2 → 1.6.0

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.
Files changed (68) hide show
  1. package/dist/index.d.ts +7 -0
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +4 -0
  4. package/dist/index.js.map +1 -1
  5. package/dist/src/core/app/Application.d.ts +2 -0
  6. package/dist/src/core/app/Application.d.ts.map +1 -1
  7. package/dist/src/core/app/Application.js +3 -0
  8. package/dist/src/core/app/Application.js.map +1 -1
  9. package/dist/src/core/bootstrap/providers/CoreServiceProvider.d.ts.map +1 -1
  10. package/dist/src/core/bootstrap/providers/CoreServiceProvider.js +9 -0
  11. package/dist/src/core/bootstrap/providers/CoreServiceProvider.js.map +1 -1
  12. package/dist/src/core/bootstrap/providers/default-providers.js +1 -1
  13. package/dist/src/core/bootstrap/providers/default-providers.js.map +1 -1
  14. package/dist/src/core/contracts/ApplicationInterface.d.ts +2 -0
  15. package/dist/src/core/contracts/ApplicationInterface.d.ts.map +1 -1
  16. package/dist/src/core/contracts/HelperInterface.d.ts +3 -0
  17. package/dist/src/core/contracts/HelperInterface.d.ts.map +1 -1
  18. package/dist/src/core/helpers/ApiClient.d.ts +1 -1
  19. package/dist/src/core/helpers/ApiClient.d.ts.map +1 -1
  20. package/dist/src/core/helpers/ApiClient.js +1 -11
  21. package/dist/src/core/helpers/ApiClient.js.map +1 -1
  22. package/dist/src/core/helpers/auth.d.ts +4 -0
  23. package/dist/src/core/helpers/auth.d.ts.map +1 -0
  24. package/dist/src/core/helpers/auth.js +10 -0
  25. package/dist/src/core/helpers/auth.js.map +1 -0
  26. package/dist/src/core/services/AuthClient.d.ts +54 -0
  27. package/dist/src/core/services/AuthClient.d.ts.map +1 -0
  28. package/dist/src/core/services/AuthClient.js +155 -0
  29. package/dist/src/core/services/AuthClient.js.map +1 -0
  30. package/dist/src/core/services/AuthService.d.ts +70 -0
  31. package/dist/src/core/services/AuthService.d.ts.map +1 -0
  32. package/dist/src/core/services/AuthService.js +157 -0
  33. package/dist/src/core/services/AuthService.js.map +1 -0
  34. package/dist/src/core/services/HelperService.d.ts +4 -0
  35. package/dist/src/core/services/HelperService.d.ts.map +1 -1
  36. package/dist/src/core/services/HelperService.js +9 -0
  37. package/dist/src/core/services/HelperService.js.map +1 -1
  38. package/dist/src/core/services/HttpService.d.ts.map +1 -1
  39. package/dist/src/core/services/HttpService.js +20 -0
  40. package/dist/src/core/services/HttpService.js.map +1 -1
  41. package/dist/src/core/services/ReactiveDependency.d.ts +15 -0
  42. package/dist/src/core/services/ReactiveDependency.d.ts.map +1 -0
  43. package/dist/src/core/services/ReactiveDependency.js +46 -0
  44. package/dist/src/core/services/ReactiveDependency.js.map +1 -0
  45. package/dist/src/core/services/index.d.ts +2 -0
  46. package/dist/src/core/services/index.d.ts.map +1 -1
  47. package/dist/src/core/services/index.js +2 -0
  48. package/dist/src/core/services/index.js.map +1 -1
  49. package/dist/src/core/view/ViewState.d.ts +1 -0
  50. package/dist/src/core/view/ViewState.d.ts.map +1 -1
  51. package/dist/src/core/view/ViewState.js +37 -2
  52. package/dist/src/core/view/ViewState.js.map +1 -1
  53. package/index.ts +7 -0
  54. package/package.json +1 -1
  55. package/src/core/app/Application.ts +6 -1
  56. package/src/core/bootstrap/providers/CoreServiceProvider.ts +7 -0
  57. package/src/core/bootstrap/providers/default-providers.ts +1 -1
  58. package/src/core/contracts/ApplicationInterface.ts +2 -0
  59. package/src/core/contracts/HelperInterface.ts +3 -0
  60. package/src/core/helpers/ApiClient.ts +1 -11
  61. package/src/core/helpers/auth.ts +10 -0
  62. package/src/core/services/AuthClient.ts +163 -0
  63. package/src/core/services/AuthService.ts +177 -0
  64. package/src/core/services/HelperService.ts +10 -0
  65. package/src/core/services/HttpService.ts +21 -0
  66. package/src/core/services/ReactiveDependency.ts +46 -0
  67. package/src/core/services/index.ts +2 -0
  68. package/src/core/view/ViewState.ts +34 -3
@@ -0,0 +1,177 @@
1
+ import { trackDependency } from './ReactiveDependency.js';
2
+ import { AuthClient } from './AuthClient.js';
3
+
4
+ export type AuthUser = Readonly<{ id: string | number; [key: string]: any }>;
5
+ export interface AuthSnapshot {
6
+ version: 1;
7
+ defaultGuard: string;
8
+ guards: Record<string, { user: AuthUser | null }>;
9
+ }
10
+ export interface AuthChange {
11
+ guard: string;
12
+ user: AuthUser | null;
13
+ previousUser: AuthUser | null;
14
+ source: 'login' | 'logout' | 'sync';
15
+ }
16
+ export type AuthEvent = 'login' | 'logout' | 'change';
17
+ type Listener = (change: AuthChange) => void;
18
+ type Context = {
19
+ defaultGuard: string;
20
+ users: Map<string, AuthUser | null>;
21
+ instances: Map<string, AuthService>;
22
+ listeners: Set<{ guard: string | null; event: AuthEvent; callback: Listener }>;
23
+ revision: number;
24
+ client?: AuthClient;
25
+ transitions: number;
26
+ };
27
+
28
+ /** Client view of server authentication. login/logout record successful server actions; they make no HTTP requests. */
29
+ export class AuthService {
30
+ private readonly context: Context;
31
+ private readonly guardName: string | null;
32
+
33
+ constructor(user: AuthUser | null = null, defaultGuard = 'web', context?: Context, guardName: string | null = null) {
34
+ this.context = context ?? {
35
+ defaultGuard, users: new Map([[defaultGuard, AuthService.copyUser(user)]]),
36
+ instances: new Map(), listeners: new Set(), revision: 0, transitions: 0,
37
+ };
38
+ this.guardName = guardName;
39
+ }
40
+
41
+ static fromSnapshot(snapshot?: unknown): AuthService {
42
+ const service = new AuthService();
43
+ if (snapshot !== undefined) service.applySnapshot(snapshot, true);
44
+ return service;
45
+ }
46
+
47
+ get revision(): number { return this.context.revision; }
48
+ get client(): AuthClient { return this.context.client ??= new AuthClient(this); }
49
+ get isTransitioning(): boolean { return this.context.transitions > 0; }
50
+ /** Internal transport barrier: page responses cannot overwrite an in-flight login/logout. */
51
+ beginClientTransition(): () => void {
52
+ this.context.revision++;
53
+ this.context.transitions++;
54
+ let finished = false;
55
+ return () => {
56
+ if (finished) return;
57
+ finished = true;
58
+ this.context.transitions--;
59
+ this.context.revision++;
60
+ };
61
+ }
62
+ private get name(): string { return this.guardName ?? this.context.defaultGuard; }
63
+
64
+ guard(name?: string | null): AuthService {
65
+ if (name == null) return this;
66
+ if (!name.trim()) throw new TypeError('An auth guard must have a name.');
67
+ let guard = this.context.instances.get(name);
68
+ if (!guard) {
69
+ guard = new AuthService(null, this.context.defaultGuard, this.context, name);
70
+ this.context.instances.set(name, guard);
71
+ }
72
+ return guard;
73
+ }
74
+
75
+ check(): boolean { return this.user() !== null; }
76
+ guest(): boolean { return !this.check(); }
77
+ id(): string | number | null { return this.user()?.id ?? null; }
78
+ user<T extends AuthUser = AuthUser>(): T | null {
79
+ trackDependency(this);
80
+ return (this.context.users.get(this.name) ?? null) as T | null;
81
+ }
82
+
83
+ /** Call only after the server accepted login (including registration/OAuth). */
84
+ login(user: AuthUser): this { return this.setUser(user, 'login'); }
85
+ /** Call only after the server confirmed logout or session expiry. */
86
+ logout(): this { return this.setUser(null, 'logout'); }
87
+
88
+ setUser(user: AuthUser | null, source: AuthChange['source'] = 'sync'): this {
89
+ const next = AuthService.copyUser(user);
90
+ const previous = this.user();
91
+ // Even a repeated explicit login/logout invalidates older in-flight responses.
92
+ if (source !== 'sync' || !AuthService.sameUser(previous, next)) this.context.revision++;
93
+ this.context.users.set(this.name, next);
94
+ this.emit(this.name, previous, next, source);
95
+ return this;
96
+ }
97
+
98
+ /** Authoritative page-data response. A snapshot replaces guards omitted by the server too. */
99
+ sync(snapshot: unknown, source: AuthChange['source'] = 'sync'): this { return this.applySnapshot(snapshot, false, source); }
100
+ /** Bootstrap only: restore the server session without reporting a new login. */
101
+ initialize(snapshot: unknown): this { return this.applySnapshot(snapshot, true); }
102
+
103
+ on(event: AuthEvent, callback: Listener): () => void {
104
+ const listener = { guard: this.guardName, event, callback };
105
+ this.context.listeners.add(listener);
106
+ return () => { this.context.listeners.delete(listener); };
107
+ }
108
+ subscribe(callback: Listener): () => void { return this.on('change', callback); }
109
+ destroy(): void { this.context.client?.destroy(); this.context.revision++; this.context.listeners.clear(); this.context.instances.clear(); this.context.users.clear(); }
110
+
111
+ private applySnapshot(snapshot: unknown, silent: boolean, source: AuthChange['source'] = 'sync'): this {
112
+ if (!AuthService.isSnapshot(snapshot)) throw new TypeError('Invalid auth snapshot.');
113
+ // Validate everything before replacing any current state.
114
+ const users = new Map(Object.entries(snapshot.guards).map(([name, guard]) => [name, AuthService.copyUser(guard.user)]));
115
+ const previous = this.context.users;
116
+ const previousDefault = this.context.defaultGuard;
117
+ this.context.users = users;
118
+ this.context.defaultGuard = snapshot.defaultGuard;
119
+ if (source !== 'sync' || previousDefault !== snapshot.defaultGuard || [...new Set([...previous.keys(), ...users.keys()])]
120
+ .some(name => !AuthService.sameUser(previous.get(name) ?? null, users.get(name) ?? null))) this.context.revision++;
121
+ if (!silent) {
122
+ for (const name of new Set([...previous.keys(), ...users.keys()])) {
123
+ this.emit(name, previous.get(name) ?? null, users.get(name) ?? null, source);
124
+ }
125
+ if (previousDefault !== snapshot.defaultGuard && AuthService.sameUser(previous.get(snapshot.defaultGuard) ?? null, users.get(snapshot.defaultGuard) ?? null)) {
126
+ this.emit(snapshot.defaultGuard, previous.get(previousDefault) ?? null, users.get(snapshot.defaultGuard) ?? null, source);
127
+ }
128
+ }
129
+ return this;
130
+ }
131
+
132
+ private emit(guard: string, previousUser: AuthUser | null, user: AuthUser | null, source: AuthChange['source']): void {
133
+ if (AuthService.sameUser(previousUser, user)) return;
134
+ const change = { guard, previousUser, user, source };
135
+ const identityChanged = previousUser?.id !== user?.id;
136
+ const events: AuthEvent[] = [];
137
+ if (identityChanged && previousUser) events.push('logout');
138
+ if (identityChanged && user) events.push('login');
139
+ events.push('change');
140
+ for (const event of events) {
141
+ for (const listener of [...this.context.listeners]) {
142
+ if (!this.context.listeners.has(listener) || listener.event !== event
143
+ || (listener.guard !== null && listener.guard !== guard)) continue;
144
+ try { listener.callback(change); }
145
+ catch (error) { console.error('[AuthService] Listener error:', error); }
146
+ }
147
+ }
148
+ }
149
+
150
+ static isSnapshot(value: any): value is AuthSnapshot {
151
+ return value?.version === 1 && typeof value.defaultGuard === 'string' && value.defaultGuard.length > 0
152
+ && value.guards !== null && typeof value.guards === 'object' && !Array.isArray(value.guards)
153
+ && Object.prototype.hasOwnProperty.call(value.guards, value.defaultGuard)
154
+ && Object.values(value.guards).every((guard: any) => guard && Object.prototype.hasOwnProperty.call(guard, 'user')
155
+ && (guard.user === null || (typeof guard.user === 'object' && !Array.isArray(guard.user)
156
+ && (typeof guard.user.id === 'string' || typeof guard.user.id === 'number'))));
157
+ }
158
+ private static copyUser(user: AuthUser | null): AuthUser | null {
159
+ if (user === null) return null;
160
+ if (!user || Array.isArray(user) || (typeof user.id !== 'string' && typeof user.id !== 'number')
161
+ || (typeof user.id === 'number' && !Number.isFinite(user.id))) {
162
+ throw new TypeError('An authenticated user must have an id.');
163
+ }
164
+ const copy = structuredClone(user);
165
+ JSON.stringify(copy); // Reject non-JSON user data before changing any state.
166
+ const seen = new WeakSet<object>();
167
+ const freeze = (value: any): void => {
168
+ if (!value || typeof value !== 'object' || seen.has(value)) return;
169
+ seen.add(value);
170
+ for (const item of Object.values(value)) freeze(item);
171
+ Object.freeze(value);
172
+ };
173
+ freeze(copy);
174
+ return copy;
175
+ }
176
+ private static sameUser(a: AuthUser | null, b: AuthUser | null): boolean { return JSON.stringify(a) === JSON.stringify(b); }
177
+ }
@@ -2,6 +2,7 @@ import { TranslationService, type TranslationConfig } from './TranslationService
2
2
  import { CollectionProxyInterface, HelperInterface } from "../contracts/HelperInterface.js";
3
3
  import { ApplicationInterface } from "../contracts/ApplicationInterface.js";
4
4
  import { app } from "../helpers/app.js";
5
+ import { AuthService } from './AuthService.js';
5
6
 
6
7
  /**
7
8
  * HelperService — PHP-compatible helper functions for SaoView runtime.
@@ -46,6 +47,15 @@ export class HelperService implements HelperInterface {
46
47
  if (App) this.App = App;
47
48
  }
48
49
 
50
+ auth(guard?: string | null): AuthService {
51
+ const application = this.App ?? app();
52
+ if (!application.has('Auth')) application.instance('Auth', AuthService.fromSnapshot(application.shared('auth')));
53
+ return application.get('Auth').guard(guard);
54
+ }
55
+
56
+ /** Compatibility for previously compiled @auth/@guest views. */
57
+ isAuth(guard?: string | null): boolean { return this.auth(guard).check(); }
58
+
49
59
  app<T = any>(key?: any, value?: any): T | ApplicationInterface
50
60
  {
51
61
  return app(key, value);
@@ -15,6 +15,9 @@
15
15
  * });
16
16
  */
17
17
 
18
+ import { app } from '../helpers/app.js';
19
+ import { AuthService } from './AuthService.js';
20
+
18
21
  // ─── Types ──────────────────────────────────────────────────────
19
22
 
20
23
  export interface HttpRequestConfig extends RequestInit {
@@ -171,10 +174,28 @@ export class HttpService {
171
174
  }
172
175
  if (!group) this.pending.set(requestKey, group = new Set());
173
176
  group.add(controller);
177
+ const auth = app.has('Auth') ? app<AuthService>('Auth') : null;
178
+ const authRevision = auth?.revision;
179
+ // Read at send time: session login/logout rotates Laravel's CSRF token.
180
+ if (typeof window !== 'undefined' && urlObj.origin === window.location.origin
181
+ && !['GET', 'HEAD', 'OPTIONS'].includes(config.method!)) {
182
+ const headers = config.headers ??= {};
183
+ const explicit = Object.keys(headers).some(key => ['x-csrf-token', 'x-xsrf-token'].includes(key.toLowerCase()));
184
+ const token = auth?.client.csrfToken ?? document.querySelector<HTMLMetaElement>('meta[name="csrf-token"]')?.content;
185
+ if (!explicit && token) headers['X-CSRF-TOKEN'] = token;
186
+ }
174
187
  const raw = await fetch(fullUrl, config);
175
188
  const responseData = await raw.json().catch(() => ({}));
176
189
  controller.signal.throwIfAborted();
177
190
 
191
+ // Only our own server may synchronize auth; an older request cannot undo a newer login/logout.
192
+ if (auth && !auth.isTransitioning && auth.revision === authRevision && typeof window !== 'undefined'
193
+ && urlObj.origin === window.location.origin
194
+ && (!raw.url || new URL(raw.url).origin === window.location.origin)
195
+ && AuthService.isSnapshot(responseData?.auth)) {
196
+ auth.sync(responseData.auth);
197
+ }
198
+
178
199
  const viewContext = responseData?.viewContext;
179
200
  const currentContext = typeof window !== 'undefined'
180
201
  ? (window as any).APP_CONFIGS?.view?.systemData?.__context__
@@ -0,0 +1,46 @@
1
+ /** A service can participate in computed evaluation without exposing its storage. */
2
+ export interface ReactiveDependency {
3
+ subscribe(callback: () => void): () => void;
4
+ }
5
+
6
+ let collecting: Set<ReactiveDependency> | null = null;
7
+
8
+ export function trackDependency(source: ReactiveDependency): void {
9
+ collecting?.add(source);
10
+ }
11
+
12
+ /** Reconciles subscriptions after each synchronous evaluation, including branch changes. */
13
+ export class DependencyTracker {
14
+ private subscriptions = new Map<ReactiveDependency, () => void>();
15
+ private destroyed = false;
16
+
17
+ constructor(private readonly invalidate: () => void) {}
18
+
19
+ evaluate<T>(read: () => T): T {
20
+ const previous = collecting;
21
+ const sources = new Set<ReactiveDependency>();
22
+ collecting = sources;
23
+ try { return read(); }
24
+ finally {
25
+ collecting = previous;
26
+ if (!this.destroyed) {
27
+ for (const [source, off] of this.subscriptions) {
28
+ if (!sources.has(source)) { off(); this.subscriptions.delete(source); }
29
+ }
30
+ for (const source of sources) {
31
+ if (!this.subscriptions.has(source)) {
32
+ this.subscriptions.set(source, source.subscribe(() => {
33
+ if (!this.destroyed) this.invalidate();
34
+ }));
35
+ }
36
+ }
37
+ }
38
+ }
39
+ }
40
+
41
+ destroy(): void {
42
+ this.destroyed = true;
43
+ for (const off of this.subscriptions.values()) off();
44
+ this.subscriptions.clear();
45
+ }
46
+ }
@@ -5,6 +5,8 @@ export * from "./HeadService.js";
5
5
  export * from "./SectionManager.js";
6
6
  export * from "./LoggerService.js";
7
7
  export * from './HelperService.js';
8
+ export * from './AuthService.js';
9
+ export * from './AuthClient.js';
8
10
  export * from './HttpService.js';
9
11
  export * from './MarkerRegistry.js';
10
12
  export * from "./StoreService.js";
@@ -1,6 +1,9 @@
1
1
  import type { ViewControllerInterface } from "../contracts/ViewControllerInterface.js";
2
2
  import type { StateManagerInterface, ViewStateInterface, StateItem, StateListener } from "../contracts/ViewStateInterface.js";
3
3
  import devtools from "../devtools/hook.js";
4
+ import { DependencyTracker } from '../services/ReactiveDependency.js';
5
+ import { AuthService } from '../services/AuthService.js';
6
+ import { app } from '../helpers/app.js';
4
7
 
5
8
  type MutationChannel = {
6
9
  notify: (() => void) | null;
@@ -188,8 +191,9 @@ export class StateManager implements StateManagerInterface {
188
191
  return this.useState(value, key)[1];
189
192
  }
190
193
 
191
- private computedNodes = new Map<string, { deps: string[]; dirty: () => void }>();
194
+ private computedNodes = new Map<string, { deps: string[]; dirty: () => void; tracker: DependencyTracker }>();
192
195
  private computedDependents = new Map<string | number, Set<string>>();
196
+ private authUnsubscribe: (() => void) | null = null;
193
197
 
194
198
  /** Invalidate the dependency graph synchronously; evaluation remains lazy. */
195
199
  private invalidateComputed(key: string | number, notify = true): void {
@@ -234,6 +238,7 @@ export class StateManager implements StateManagerInterface {
234
238
  if (deps.some(dep => reaches(dep))) throw new Error(`[ViewState] Computed dependency cycle: ${key}`);
235
239
  const existing = this.states[key] as any;
236
240
  if (existing && !existing.__computed__) throw new Error(`[ViewState] State already exists: ${key}`);
241
+ this.computedNodes.get(key)?.tracker.destroy();
237
242
  for (const dep of this.computedNodes.get(key)?.deps ?? []) {
238
243
  const dependents = this.computedDependents.get(dep);
239
244
  dependents?.delete(key);
@@ -242,6 +247,10 @@ export class StateManager implements StateManagerInterface {
242
247
  let cache: T;
243
248
  let dirty = true;
244
249
  let evaluating = false;
250
+ const tracker = new DependencyTracker(() => {
251
+ dirty = true;
252
+ this.enqueueChange(key);
253
+ });
245
254
  const slot: any = {
246
255
  key,
247
256
  __computed__: true,
@@ -252,7 +261,7 @@ export class StateManager implements StateManagerInterface {
252
261
  if (dirty) {
253
262
  if (evaluating) throw new Error(`[ViewState] Recursive computed read: ${key}`);
254
263
  evaluating = true;
255
- try { cache = fn(); dirty = false; }
264
+ try { cache = tracker.evaluate(fn); dirty = false; }
256
265
  finally { evaluating = false; }
257
266
  }
258
267
  return cache;
@@ -262,7 +271,7 @@ export class StateManager implements StateManagerInterface {
262
271
  });
263
272
  this.states[key] = slot;
264
273
  this.setters[key] = slot.setValue;
265
- this.computedNodes.set(key, { deps: [...new Set(deps)], dirty: () => { dirty = true; } });
274
+ this.computedNodes.set(key, { deps: [...new Set(deps)], dirty: () => { dirty = true; }, tracker });
266
275
  for (const dep of deps) {
267
276
  if (!this.computedDependents.has(dep)) this.computedDependents.set(dep, new Set());
268
277
  this.computedDependents.get(dep)!.add(key);
@@ -367,6 +376,21 @@ export class StateManager implements StateManagerInterface {
367
376
  if (!records) this.subscriptions.set(k, records = new Set());
368
377
  records.add(record);
369
378
  }
379
+ if (keys.has('@auth') && !this.authUnsubscribe) {
380
+ const application = this.controller?.App ?? app();
381
+ if (!application.has('Auth')) {
382
+ application.instance('Auth', AuthService.fromSnapshot(application.shared('auth')));
383
+ }
384
+ this.authUnsubscribe = (application.get('Auth') as AuthService).subscribe(() => this.enqueueChange('@auth'));
385
+ }
386
+ // Hydration may claim an SSR text node without evaluating its factory.
387
+ // Observed computed values still need their service dependencies connected.
388
+ try {
389
+ for (const k of keys) if (this.computedNodes.has(k)) this.getStateByKey(k);
390
+ } catch (error) {
391
+ this.removeSubscription(record);
392
+ throw error;
393
+ }
370
394
  return () => this.removeSubscription(record);
371
395
  }
372
396
 
@@ -378,6 +402,10 @@ export class StateManager implements StateManagerInterface {
378
402
  records?.delete(record);
379
403
  if (records?.size === 0) this.subscriptions.delete(k);
380
404
  }
405
+ if (!this.subscriptions.has('@auth')) {
406
+ this.authUnsubscribe?.();
407
+ this.authUnsubscribe = null;
408
+ }
381
409
  }
382
410
 
383
411
  unsubscribe(
@@ -1079,6 +1107,9 @@ export class StateManager implements StateManagerInterface {
1079
1107
  cancelAnimationFrame(this.flushRAF);
1080
1108
  this.flushRAF = null;
1081
1109
  }
1110
+ this.authUnsubscribe?.();
1111
+ this.authUnsubscribe = null;
1112
+ for (const node of this.computedNodes.values()) node.tracker.destroy();
1082
1113
  this.computedNodes.clear();
1083
1114
  this.computedDependents.clear();
1084
1115
  this.versions.clear();