@saolabs/client 1.5.2 → 1.7.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 +9 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/src/core/app/Application.d.ts +2 -0
- package/dist/src/core/app/Application.d.ts.map +1 -1
- package/dist/src/core/app/Application.js +3 -0
- package/dist/src/core/app/Application.js.map +1 -1
- package/dist/src/core/bootstrap/bundle.d.ts +1 -1
- package/dist/src/core/bootstrap/bundle.d.ts.map +1 -1
- package/dist/src/core/bootstrap/bundle.js +14 -1
- package/dist/src/core/bootstrap/bundle.js.map +1 -1
- package/dist/src/core/bootstrap/providers/CoreServiceProvider.d.ts.map +1 -1
- package/dist/src/core/bootstrap/providers/CoreServiceProvider.js +9 -0
- package/dist/src/core/bootstrap/providers/CoreServiceProvider.js.map +1 -1
- package/dist/src/core/bootstrap/providers/default-providers.js +1 -1
- package/dist/src/core/bootstrap/providers/default-providers.js.map +1 -1
- package/dist/src/core/contracts/ApplicationInterface.d.ts +2 -0
- package/dist/src/core/contracts/ApplicationInterface.d.ts.map +1 -1
- package/dist/src/core/contracts/BundleInterface.d.ts +5 -0
- package/dist/src/core/contracts/BundleInterface.d.ts.map +1 -1
- package/dist/src/core/contracts/HelperInterface.d.ts +3 -0
- package/dist/src/core/contracts/HelperInterface.d.ts.map +1 -1
- package/dist/src/core/contracts/RouterInterface.d.ts +2 -0
- package/dist/src/core/contracts/RouterInterface.d.ts.map +1 -1
- package/dist/src/core/contracts/ViewManagerInterface.d.ts +2 -0
- package/dist/src/core/contracts/ViewManagerInterface.d.ts.map +1 -1
- package/dist/src/core/helpers/ApiClient.d.ts +1 -1
- package/dist/src/core/helpers/ApiClient.d.ts.map +1 -1
- package/dist/src/core/helpers/ApiClient.js +1 -11
- package/dist/src/core/helpers/ApiClient.js.map +1 -1
- package/dist/src/core/helpers/auth.d.ts +4 -0
- package/dist/src/core/helpers/auth.d.ts.map +1 -0
- package/dist/src/core/helpers/auth.js +10 -0
- package/dist/src/core/helpers/auth.js.map +1 -0
- package/dist/src/core/routers/ClientMiddleware.d.ts +45 -0
- package/dist/src/core/routers/ClientMiddleware.d.ts.map +1 -0
- package/dist/src/core/routers/ClientMiddleware.js +113 -0
- package/dist/src/core/routers/ClientMiddleware.js.map +1 -0
- package/dist/src/core/routers/Router.d.ts +6 -0
- package/dist/src/core/routers/Router.d.ts.map +1 -1
- package/dist/src/core/routers/Router.js +61 -5
- package/dist/src/core/routers/Router.js.map +1 -1
- package/dist/src/core/services/AuthClient.d.ts +54 -0
- package/dist/src/core/services/AuthClient.d.ts.map +1 -0
- package/dist/src/core/services/AuthClient.js +155 -0
- package/dist/src/core/services/AuthClient.js.map +1 -0
- package/dist/src/core/services/AuthService.d.ts +70 -0
- package/dist/src/core/services/AuthService.d.ts.map +1 -0
- package/dist/src/core/services/AuthService.js +157 -0
- package/dist/src/core/services/AuthService.js.map +1 -0
- package/dist/src/core/services/HelperService.d.ts +4 -0
- package/dist/src/core/services/HelperService.d.ts.map +1 -1
- package/dist/src/core/services/HelperService.js +9 -0
- package/dist/src/core/services/HelperService.js.map +1 -1
- package/dist/src/core/services/HttpService.d.ts.map +1 -1
- package/dist/src/core/services/HttpService.js +20 -0
- package/dist/src/core/services/HttpService.js.map +1 -1
- package/dist/src/core/services/ReactiveDependency.d.ts +15 -0
- package/dist/src/core/services/ReactiveDependency.d.ts.map +1 -0
- package/dist/src/core/services/ReactiveDependency.js +46 -0
- package/dist/src/core/services/ReactiveDependency.js.map +1 -0
- package/dist/src/core/services/index.d.ts +2 -0
- package/dist/src/core/services/index.d.ts.map +1 -1
- package/dist/src/core/services/index.js +2 -0
- package/dist/src/core/services/index.js.map +1 -1
- package/dist/src/core/view/ViewController.d.ts +2 -0
- package/dist/src/core/view/ViewController.d.ts.map +1 -1
- package/dist/src/core/view/ViewController.js +3 -0
- package/dist/src/core/view/ViewController.js.map +1 -1
- package/dist/src/core/view/ViewManager.d.ts +2 -0
- package/dist/src/core/view/ViewManager.d.ts.map +1 -1
- package/dist/src/core/view/ViewManager.js +23 -0
- package/dist/src/core/view/ViewManager.js.map +1 -1
- package/dist/src/core/view/ViewState.d.ts +1 -0
- package/dist/src/core/view/ViewState.d.ts.map +1 -1
- package/dist/src/core/view/ViewState.js +37 -2
- package/dist/src/core/view/ViewState.js.map +1 -1
- package/index.ts +10 -0
- package/package.json +1 -1
- package/src/core/app/Application.ts +6 -1
- package/src/core/bootstrap/bundle.ts +10 -1
- package/src/core/bootstrap/providers/CoreServiceProvider.ts +7 -0
- package/src/core/bootstrap/providers/default-providers.ts +1 -1
- package/src/core/contracts/ApplicationInterface.ts +2 -0
- package/src/core/contracts/BundleInterface.ts +5 -0
- package/src/core/contracts/HelperInterface.ts +3 -0
- package/src/core/contracts/RouterInterface.ts +2 -0
- package/src/core/contracts/ViewManagerInterface.ts +2 -0
- package/src/core/helpers/ApiClient.ts +1 -11
- package/src/core/helpers/auth.ts +10 -0
- package/src/core/routers/ClientMiddleware.ts +124 -0
- package/src/core/routers/Router.ts +56 -5
- package/src/core/services/AuthClient.ts +163 -0
- package/src/core/services/AuthService.ts +177 -0
- package/src/core/services/HelperService.ts +10 -0
- package/src/core/services/HttpService.ts +21 -0
- package/src/core/services/ReactiveDependency.ts +46 -0
- package/src/core/services/index.ts +2 -0
- package/src/core/view/ViewController.ts +3 -0
- package/src/core/view/ViewManager.ts +23 -0
- package/src/core/view/ViewState.ts +34 -3
|
@@ -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";
|
|
@@ -84,6 +84,8 @@ export class ViewController implements ViewControllerInterface {
|
|
|
84
84
|
public superView: ViewControllerInterface | null = null;
|
|
85
85
|
/** Path to layout (super) view — e.g. 'layouts.main' */
|
|
86
86
|
public superViewPath: string | null = null;
|
|
87
|
+
/** Data last passed to the layout by `@extends` — re-applied when the page is restored from PageCache (render() does not run then). */
|
|
88
|
+
public superViewData: Record<string, any> | null = null;
|
|
87
89
|
/** Whether this controller IS a layout */
|
|
88
90
|
public isSuperView: boolean = false;
|
|
89
91
|
/** For layouts: the original page view's controller (for block mounting) */
|
|
@@ -1497,6 +1499,7 @@ export class ViewController implements ViewControllerInterface {
|
|
|
1497
1499
|
|
|
1498
1500
|
extendView(path: string, data: Record<string, any> = {}): ViewInterface | null {
|
|
1499
1501
|
this.superViewPath = path;
|
|
1502
|
+
this.superViewData = data;
|
|
1500
1503
|
// Sync: render() trả superView ngay (xem ViewManager.resolveViewSync).
|
|
1501
1504
|
const superView: ViewInterface = this.App.View?.resolveViewSync(path, data, true);
|
|
1502
1505
|
if (superView) {
|
|
@@ -390,6 +390,18 @@ export class ViewManager implements ViewManagerInterface {
|
|
|
390
390
|
return this.hasView(name);
|
|
391
391
|
}
|
|
392
392
|
|
|
393
|
+
/** Read compiler/builder metadata without invoking setup or importing a lazy chunk. */
|
|
394
|
+
getViewMiddleware(name: string): readonly string[] {
|
|
395
|
+
const key = this.resolveRegistryKey(name);
|
|
396
|
+
const factory = this.viewRegistry[key] ?? this.resolvedFactories.get(key);
|
|
397
|
+
const middleware = (factory as any)?.middleware;
|
|
398
|
+
if (middleware === undefined) return [];
|
|
399
|
+
if (!Array.isArray(middleware) || middleware.some(ref => typeof ref !== 'string')) {
|
|
400
|
+
throw new TypeError(`Invalid middleware metadata on view "${name}".`);
|
|
401
|
+
}
|
|
402
|
+
return [...middleware];
|
|
403
|
+
}
|
|
404
|
+
|
|
393
405
|
/**
|
|
394
406
|
* generateViewId — tạo unique ID cho mỗi view instance.
|
|
395
407
|
*
|
|
@@ -1464,6 +1476,17 @@ export class ViewManager implements ViewManagerInterface {
|
|
|
1464
1476
|
}
|
|
1465
1477
|
|
|
1466
1478
|
this.blockManager.restorePageContent(pageView.__ctrl__.viewId, entry.outletContents);
|
|
1479
|
+
// render() không chạy lại khi restore → extendView() không gọi: layout dùng
|
|
1480
|
+
// chung còn giữ dữ liệu @extends của trang trước. Áp lại dữ liệu của trang này.
|
|
1481
|
+
// Lưu ý: updateData chỉ GỘP — khoá trang không truyền vẫn giữ giá trị cũ
|
|
1482
|
+
// (mặc định @vars nằm trong closure compiled), nên trang nên truyền tường minh.
|
|
1483
|
+
for (const v of entry.views) {
|
|
1484
|
+
const ctrl = v.__ctrl__ as any;
|
|
1485
|
+
const superView = ctrl.superView;
|
|
1486
|
+
if (superView && ctrl.superViewData && layoutChain.some(l => l.__ctrl__ === superView)) {
|
|
1487
|
+
superView.updateData?.(ctrl.superViewData);
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1467
1490
|
for (const v of entry.views) {
|
|
1468
1491
|
v.__ctrl__.resume();
|
|
1469
1492
|
}
|
|
@@ -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
|
|
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();
|