@orieljs/live 0.1.0-alpha.3 → 0.1.0-alpha.5
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/NOTICE +9 -0
- package/README.md +45 -1
- package/dist/index.d.ts +92 -8
- package/dist/index.js +356 -52
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
package/dist/index.js
CHANGED
|
@@ -219,8 +219,24 @@ import { inject as inject3 } from "@orieljs/container";
|
|
|
219
219
|
// Testable.ts
|
|
220
220
|
import { AssertionError } from "assert";
|
|
221
221
|
import { isDeepStrictEqual } from "util";
|
|
222
|
-
import { LiveManager as LiveManager2 } from "@orieljs/contracts";
|
|
223
|
-
import { inject as inject2 } from "@orieljs/container";
|
|
222
|
+
import { LiveManager as LiveManager2, Session, token } from "@orieljs/contracts";
|
|
223
|
+
import { Container, currentContainer, inject as inject2 } from "@orieljs/container";
|
|
224
|
+
|
|
225
|
+
// flash.ts
|
|
226
|
+
function trackFlash(session, { saved = true } = {}) {
|
|
227
|
+
if (session === void 0) return () => void 0;
|
|
228
|
+
const before = new Set(session.get("_flash.new", []));
|
|
229
|
+
return (redirected) => {
|
|
230
|
+
if (redirected) return;
|
|
231
|
+
const flashed = session.get("_flash.new", []);
|
|
232
|
+
const fresh = flashed.filter((key) => !before.has(key));
|
|
233
|
+
if (fresh.length === 0) return;
|
|
234
|
+
session.put("_flash.new", flashed.filter((key) => before.has(key)));
|
|
235
|
+
if (!saved) return session.forget(...fresh);
|
|
236
|
+
const old = session.get("_flash.old", []);
|
|
237
|
+
session.put("_flash.old", [...old, ...fresh.filter((key) => !old.includes(key))]);
|
|
238
|
+
};
|
|
239
|
+
}
|
|
224
240
|
|
|
225
241
|
// errors.ts
|
|
226
242
|
var LiveError = class extends Error {
|
|
@@ -284,14 +300,14 @@ function rootTag(html, component) {
|
|
|
284
300
|
let rootEnd = -1;
|
|
285
301
|
TOKEN.lastIndex = 0;
|
|
286
302
|
for (let match = TOKEN.exec(source); match !== null; match = TOKEN.exec(source)) {
|
|
287
|
-
const [
|
|
303
|
+
const [token2, closing, opening, selfClosing] = match;
|
|
288
304
|
if (root === void 0 && match.index !== 0) fail2();
|
|
289
305
|
if (depth === 0 && root !== void 0) fail2();
|
|
290
306
|
if (opening !== void 0) {
|
|
291
307
|
const tag = opening.toLowerCase();
|
|
292
308
|
root ??= tag;
|
|
293
309
|
if (selfClosing === "/" || VOID.has(tag)) {
|
|
294
|
-
if (depth === 0) rootEnd = match.index +
|
|
310
|
+
if (depth === 0) rootEnd = match.index + token2.length;
|
|
295
311
|
continue;
|
|
296
312
|
}
|
|
297
313
|
if (RAW_TEXT.has(tag)) {
|
|
@@ -303,7 +319,7 @@ function rootTag(html, component) {
|
|
|
303
319
|
depth++;
|
|
304
320
|
} else if (closing !== void 0) {
|
|
305
321
|
if (--depth < 0) fail2();
|
|
306
|
-
if (depth === 0) rootEnd = match.index +
|
|
322
|
+
if (depth === 0) rootEnd = match.index + token2.length;
|
|
307
323
|
}
|
|
308
324
|
}
|
|
309
325
|
if (root === void 0 || depth !== 0 || source.slice(rootEnd).trim() !== "") fail2();
|
|
@@ -322,23 +338,40 @@ var fail = (message, actual, expected) => {
|
|
|
322
338
|
throw new AssertionError({ message, actual, expected, operator: "live" });
|
|
323
339
|
};
|
|
324
340
|
var getPath = (target, path) => path.split(".").reduce((node, segment) => typeof node === "object" && node !== null ? node[segment] : void 0, target);
|
|
341
|
+
var TestRequestScope = token("Testing.RequestScope");
|
|
342
|
+
function requestScope() {
|
|
343
|
+
const container = currentContainer() ?? Container.getInstance();
|
|
344
|
+
return container?.bound(TestRequestScope) === true ? container.make(TestRequestScope) : (run) => run();
|
|
345
|
+
}
|
|
346
|
+
function currentSession() {
|
|
347
|
+
const container = currentContainer();
|
|
348
|
+
if (container?.bound(Session) !== true) return void 0;
|
|
349
|
+
try {
|
|
350
|
+
return container.make(Session);
|
|
351
|
+
} catch {
|
|
352
|
+
return void 0;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
325
355
|
var Testable = class _Testable {
|
|
326
|
-
constructor(manager, result) {
|
|
356
|
+
constructor(manager, scope, result) {
|
|
327
357
|
this.manager = manager;
|
|
358
|
+
this.scope = scope;
|
|
328
359
|
this.#snapshot = JSON.stringify(result.snapshot);
|
|
329
360
|
this.#html = result.html ?? "";
|
|
330
361
|
this.#effects = result.effects;
|
|
331
362
|
this.#instance = result.instance;
|
|
332
363
|
}
|
|
333
364
|
manager;
|
|
365
|
+
scope;
|
|
334
366
|
#snapshot;
|
|
335
367
|
#html;
|
|
336
368
|
#effects;
|
|
337
369
|
#instance;
|
|
338
370
|
/** Mounts `component` with `params`, as `<Live component={…} {...params} />` would. */
|
|
339
371
|
static async create(component, params = {}) {
|
|
372
|
+
const scope = requestScope();
|
|
340
373
|
const manager = inject2(LiveManager2);
|
|
341
|
-
return new _Testable(manager, await manager.mountComponent(component, params));
|
|
374
|
+
return new _Testable(manager, scope, await scope(() => manager.mountComponent(component, params)));
|
|
342
375
|
}
|
|
343
376
|
/** The component instance after the last request. */
|
|
344
377
|
instance() {
|
|
@@ -373,7 +406,12 @@ var Testable = class _Testable {
|
|
|
373
406
|
return this.#update({}, [{ method: "$refresh", params: [] }]);
|
|
374
407
|
}
|
|
375
408
|
async #update(updates, calls) {
|
|
376
|
-
const result = await this.
|
|
409
|
+
const result = await this.scope(async () => {
|
|
410
|
+
const flashed = trackFlash(currentSession(), { saved: false });
|
|
411
|
+
const updated = await this.manager.updateComponent(this.#snapshot, updates, calls);
|
|
412
|
+
flashed(updated.effects.redirect !== void 0);
|
|
413
|
+
return updated;
|
|
414
|
+
});
|
|
377
415
|
this.#snapshot = JSON.stringify(result.snapshot);
|
|
378
416
|
if (result.html !== void 0) this.#html = result.html;
|
|
379
417
|
this.#effects = result.effects;
|
|
@@ -469,18 +507,20 @@ var Live = Object.assign(LiveComponent, {
|
|
|
469
507
|
});
|
|
470
508
|
|
|
471
509
|
// LiveComponentManager.ts
|
|
472
|
-
import { AsyncLocalStorage } from "async_hooks";
|
|
510
|
+
import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
|
|
473
511
|
import { randomBytes } from "crypto";
|
|
474
512
|
import {
|
|
475
513
|
ClassRegistry,
|
|
476
514
|
ConfigRepository,
|
|
477
|
-
CurrentRoute,
|
|
515
|
+
CurrentRoute as CurrentRoute2,
|
|
478
516
|
Encrypter,
|
|
479
517
|
HttpRequest,
|
|
480
518
|
Redirector,
|
|
519
|
+
Router,
|
|
520
|
+
Session as Session2,
|
|
481
521
|
ViewFactory
|
|
482
522
|
} from "@orieljs/contracts";
|
|
483
|
-
import { currentContainer } from "@orieljs/container";
|
|
523
|
+
import { currentContainer as currentContainer2 } from "@orieljs/container";
|
|
484
524
|
import { Str as Str2 } from "@orieljs/support";
|
|
485
525
|
|
|
486
526
|
// checksum.ts
|
|
@@ -531,6 +571,125 @@ async function discover(directory) {
|
|
|
531
571
|
return found;
|
|
532
572
|
}
|
|
533
573
|
|
|
574
|
+
// persistent.ts
|
|
575
|
+
import { CurrentRoute } from "@orieljs/contracts";
|
|
576
|
+
var PERSISTENT_MIDDLEWARE = ["auth", "verified", "guest", "can"];
|
|
577
|
+
var ROUTER_METHODS = ["getRoutesByMatchOrder", "getPatterns", "getPatternVersion", "getMiddleware", "getMiddlewareGroups", "substituteBindings"];
|
|
578
|
+
function canMatch(router) {
|
|
579
|
+
const methods = router;
|
|
580
|
+
return ROUTER_METHODS.every((name) => typeof methods[name] === "function");
|
|
581
|
+
}
|
|
582
|
+
function isMatchingRoute(route) {
|
|
583
|
+
const candidate = route;
|
|
584
|
+
return typeof candidate === "object" && candidate !== null && Array.isArray(candidate.methods) && ["match", "gatherMiddleware", "excludedMiddleware"].every((name) => typeof candidate[name] === "function");
|
|
585
|
+
}
|
|
586
|
+
function routePath(path) {
|
|
587
|
+
const trimmed = path.replace(/^\/+|\/+$/g, "").replace(/\/{2,}/g, "/");
|
|
588
|
+
return trimmed === "" ? "" : `/${trimmed}`;
|
|
589
|
+
}
|
|
590
|
+
function expand(references, aliases, groups, out = [], visiting = /* @__PURE__ */ new Set()) {
|
|
591
|
+
for (const reference of references) {
|
|
592
|
+
if (typeof reference === "function") {
|
|
593
|
+
out.push({ cls: reference, parameters: [] });
|
|
594
|
+
continue;
|
|
595
|
+
}
|
|
596
|
+
const index = reference.indexOf(":");
|
|
597
|
+
const name = index === -1 ? reference : reference.slice(0, index);
|
|
598
|
+
if (index === -1 && Object.hasOwn(groups, name)) {
|
|
599
|
+
if (visiting.has(name)) continue;
|
|
600
|
+
visiting.add(name);
|
|
601
|
+
expand(groups[name], aliases, groups, out, visiting);
|
|
602
|
+
visiting.delete(name);
|
|
603
|
+
} else if (Object.hasOwn(aliases, name)) {
|
|
604
|
+
out.push({ cls: aliases[name], parameters: index === -1 ? [] : reference.slice(index + 1).split(",") });
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
return out;
|
|
608
|
+
}
|
|
609
|
+
function persistentPass(router, persistent, method, path) {
|
|
610
|
+
if (!canMatch(router)) throw new LiveError("Live re-applies the page route's middleware to updates, which needs Oriel's router.");
|
|
611
|
+
const routes = router.getRoutesByMatchOrder();
|
|
612
|
+
if (!Array.isArray(routes) || !routes.every(isMatchingRoute)) throw new LiveError("Live re-applies the page route's middleware to updates, which needs Oriel's routes.");
|
|
613
|
+
const verb = method.toUpperCase();
|
|
614
|
+
const target = routePath(path);
|
|
615
|
+
const patterns = router.getPatterns();
|
|
616
|
+
const version = router.getPatternVersion();
|
|
617
|
+
let matched;
|
|
618
|
+
for (const candidate of routes) {
|
|
619
|
+
const values2 = candidate.methods.includes(verb) ? candidate.match(target, patterns, version) : void 0;
|
|
620
|
+
if (values2 !== void 0) {
|
|
621
|
+
matched = { route: candidate, values: values2 };
|
|
622
|
+
break;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
if (matched === void 0) return void 0;
|
|
626
|
+
const { route, values } = matched;
|
|
627
|
+
const aliases = router.getMiddleware();
|
|
628
|
+
const groups = router.getMiddlewareGroups();
|
|
629
|
+
const wanted = new Set(persistent.flatMap((name) => Object.hasOwn(aliases, name) ? [aliases[name]] : []));
|
|
630
|
+
const excluded = new Set(expand(route.excludedMiddleware(), aliases, groups).map((entry) => entry.cls));
|
|
631
|
+
const ids = /* @__PURE__ */ new Map();
|
|
632
|
+
const seen = /* @__PURE__ */ new Set();
|
|
633
|
+
const middleware = expand(route.gatherMiddleware(), aliases, groups).filter((entry) => {
|
|
634
|
+
if (!ids.has(entry.cls)) ids.set(entry.cls, ids.size);
|
|
635
|
+
const key = `${ids.get(entry.cls)}:${entry.parameters.join(",")}`;
|
|
636
|
+
if (!wanted.has(entry.cls) || excluded.has(entry.cls) || seen.has(key)) return false;
|
|
637
|
+
seen.add(key);
|
|
638
|
+
return true;
|
|
639
|
+
});
|
|
640
|
+
const early = new Set(expand(["web", "api"], aliases, groups).map((entry) => entry.cls));
|
|
641
|
+
for (const name of ["auth", "guest"]) if (Object.hasOwn(aliases, name)) early.add(aliases[name]);
|
|
642
|
+
const before = middleware.filter((entry) => early.has(entry.cls));
|
|
643
|
+
const after = middleware.filter((entry) => !early.has(entry.cls));
|
|
644
|
+
return { router, route, values, middleware: [...before, ...after], at: before.length };
|
|
645
|
+
}
|
|
646
|
+
function persistentMiddleware(router, persistent, method, path) {
|
|
647
|
+
return [...persistentPass(router, persistent, method, path)?.middleware ?? []];
|
|
648
|
+
}
|
|
649
|
+
function currentRoute(container) {
|
|
650
|
+
if (!container.bound(CurrentRoute)) return void 0;
|
|
651
|
+
try {
|
|
652
|
+
return container.make(CurrentRoute);
|
|
653
|
+
} catch {
|
|
654
|
+
return void 0;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
async function throughMiddleware(container, request, pass, destination) {
|
|
658
|
+
const original = currentRoute(container);
|
|
659
|
+
const page = { route: pass.route, parameters: { ...pass.values }, middleware: [] };
|
|
660
|
+
let swapped = false;
|
|
661
|
+
const show = () => {
|
|
662
|
+
container.instance(CurrentRoute, page);
|
|
663
|
+
swapped = true;
|
|
664
|
+
};
|
|
665
|
+
const restore = () => {
|
|
666
|
+
if (!swapped) return;
|
|
667
|
+
if (original === void 0) container.forgetInstance(CurrentRoute);
|
|
668
|
+
else container.instance(CurrentRoute, original);
|
|
669
|
+
swapped = false;
|
|
670
|
+
};
|
|
671
|
+
let ran;
|
|
672
|
+
const run = async (index) => {
|
|
673
|
+
if (index === pass.at) Object.assign(page.parameters, await pass.router.substituteBindings(pass.route, pass.values, container));
|
|
674
|
+
if (index === pass.middleware.length) {
|
|
675
|
+
restore();
|
|
676
|
+
ran = { result: await destination() };
|
|
677
|
+
return new Response(null, { status: 204 });
|
|
678
|
+
}
|
|
679
|
+
const { cls, parameters } = pass.middleware[index];
|
|
680
|
+
const instance = container.make(cls);
|
|
681
|
+
const next = () => run(index + 1);
|
|
682
|
+
return instance.handle(request, next, ...parameters);
|
|
683
|
+
};
|
|
684
|
+
show();
|
|
685
|
+
try {
|
|
686
|
+
const response = await run(0);
|
|
687
|
+
return ran ? ran.result : response;
|
|
688
|
+
} finally {
|
|
689
|
+
restore();
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
534
693
|
// security.ts
|
|
535
694
|
import { Collection as Collection2 } from "@orieljs/collections";
|
|
536
695
|
|
|
@@ -677,12 +836,72 @@ function isUpdatable(instance, path) {
|
|
|
677
836
|
return !isModel(node) && !(node instanceof Collection2);
|
|
678
837
|
}
|
|
679
838
|
|
|
839
|
+
// WithPagination.ts
|
|
840
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
841
|
+
var PAGINATED = /* @__PURE__ */ Symbol.for("oriel:live.pagination");
|
|
842
|
+
function toPage(value) {
|
|
843
|
+
const page = typeof value === "number" ? value : typeof value === "string" && value.trim() !== "" ? Number(value) : Number.NaN;
|
|
844
|
+
return Number.isFinite(page) && page >= 1 ? Math.min(Math.floor(page), Number.MAX_SAFE_INTEGER) : 1;
|
|
845
|
+
}
|
|
846
|
+
async function hook(instance, name, ...args) {
|
|
847
|
+
const method = instance[name];
|
|
848
|
+
if (typeof method === "function") await method.apply(instance, args);
|
|
849
|
+
}
|
|
850
|
+
function WithPagination(Base) {
|
|
851
|
+
class WithPaginationMixin extends Base {
|
|
852
|
+
page = 1;
|
|
853
|
+
nextPage() {
|
|
854
|
+
return this.setPage(toPage(this.page) + 1);
|
|
855
|
+
}
|
|
856
|
+
previousPage() {
|
|
857
|
+
return this.setPage(Math.max(toPage(this.page) - 1, 1));
|
|
858
|
+
}
|
|
859
|
+
gotoPage(page) {
|
|
860
|
+
return this.setPage(page);
|
|
861
|
+
}
|
|
862
|
+
resetPage() {
|
|
863
|
+
return this.setPage(1);
|
|
864
|
+
}
|
|
865
|
+
async setPage(page) {
|
|
866
|
+
const next = toPage(page);
|
|
867
|
+
await hook(this, "updatingPage", next);
|
|
868
|
+
this.page = next;
|
|
869
|
+
await hook(this, "updatedPage", next);
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
Object.defineProperty(WithPaginationMixin.prototype, PAGINATED, { value: true });
|
|
873
|
+
return WithPaginationMixin;
|
|
874
|
+
}
|
|
875
|
+
function isPaginated(component) {
|
|
876
|
+
return component[PAGINATED] === true;
|
|
877
|
+
}
|
|
878
|
+
var rendering = new AsyncLocalStorage();
|
|
879
|
+
function renderPaginated(component, url, run) {
|
|
880
|
+
return rendering.run(isPaginated(component) ? { component, url } : void 0, run);
|
|
881
|
+
}
|
|
882
|
+
function outsidePagination(run) {
|
|
883
|
+
return rendering.run(void 0, run);
|
|
884
|
+
}
|
|
885
|
+
var livePaginationState = {
|
|
886
|
+
currentPage(pageName) {
|
|
887
|
+
const current = rendering.getStore();
|
|
888
|
+
if (current === void 0 || pageName !== "page") return void 0;
|
|
889
|
+
const page = toPage(current.component.page);
|
|
890
|
+
current.component.page = page;
|
|
891
|
+
return page;
|
|
892
|
+
},
|
|
893
|
+
currentPath() {
|
|
894
|
+
return rendering.getStore()?.url;
|
|
895
|
+
}
|
|
896
|
+
};
|
|
897
|
+
|
|
680
898
|
// LiveComponentManager.ts
|
|
681
|
-
var renderContext = new
|
|
682
|
-
|
|
683
|
-
|
|
899
|
+
var renderContext = new AsyncLocalStorage2();
|
|
900
|
+
var pageRequest = new AsyncLocalStorage2();
|
|
901
|
+
function optional(container, token2) {
|
|
902
|
+
if (!container.bound(token2)) return void 0;
|
|
684
903
|
try {
|
|
685
|
-
return container.make(
|
|
904
|
+
return container.make(token2);
|
|
686
905
|
} catch {
|
|
687
906
|
return void 0;
|
|
688
907
|
}
|
|
@@ -691,7 +910,7 @@ var isRecord = (value) => typeof value === "object" && value !== null && !Array.
|
|
|
691
910
|
function setOwn(object, key, value) {
|
|
692
911
|
Object.defineProperty(object, key, { value, enumerable: true, writable: true, configurable: true });
|
|
693
912
|
}
|
|
694
|
-
async function
|
|
913
|
+
async function hook2(instance, name, ...args) {
|
|
695
914
|
const method = instance[name];
|
|
696
915
|
if (typeof method === "function") await method.apply(instance, args);
|
|
697
916
|
}
|
|
@@ -702,7 +921,7 @@ var LiveComponentManager = class {
|
|
|
702
921
|
app;
|
|
703
922
|
/** The request scope when there is one (sessions, the current route), else the application. */
|
|
704
923
|
container() {
|
|
705
|
-
return
|
|
924
|
+
return currentContainer2() ?? this.app;
|
|
706
925
|
}
|
|
707
926
|
registry() {
|
|
708
927
|
return this.container().make(ClassRegistry);
|
|
@@ -742,7 +961,7 @@ var LiveComponentManager = class {
|
|
|
742
961
|
setOwn(context.children, childKey, existing);
|
|
743
962
|
return `<${existing[0]} o:id="${escapeAttribute(existing[1])}"></${existing[0]}>`;
|
|
744
963
|
}
|
|
745
|
-
const result = await this.mountComponent(component, params);
|
|
964
|
+
const result = await outsidePagination(() => this.mountComponent(component, params));
|
|
746
965
|
setOwn(context.children, childKey, [result.tag, result.snapshot.memo.id]);
|
|
747
966
|
return result.html;
|
|
748
967
|
}
|
|
@@ -754,14 +973,15 @@ var LiveComponentManager = class {
|
|
|
754
973
|
const state = stateOf(instance);
|
|
755
974
|
state.id = randomBytes(15).toString("base64url");
|
|
756
975
|
state.name = name;
|
|
757
|
-
await
|
|
976
|
+
await hook2(instance, "boot");
|
|
758
977
|
for (const [property, value] of Object.entries(params)) {
|
|
759
978
|
if (Object.hasOwn(instance, property)) instance[property] = value;
|
|
760
979
|
}
|
|
761
980
|
this.prepareComputed(instance);
|
|
762
|
-
await hook(instance, "mount", params);
|
|
763
981
|
const request = optional(this.container(), HttpRequest);
|
|
764
|
-
const
|
|
982
|
+
const page = pageRequest.getStore() ?? { path: request?.path() ?? "", method: request?.method() ?? "GET" };
|
|
983
|
+
await renderPaginated(instance, this.pageUrl(page.path), () => hook2(instance, "mount", params));
|
|
984
|
+
const memo = { id: state.id, name, path: page.path, method: page.method, children: {}, errors: {} };
|
|
765
985
|
const { html, tag } = await this.render(instance, memo, {});
|
|
766
986
|
const effects = this.effects(instance);
|
|
767
987
|
const listeners = Object.keys(listenersOf(cls));
|
|
@@ -774,7 +994,8 @@ var LiveComponentManager = class {
|
|
|
774
994
|
async update(request) {
|
|
775
995
|
const components = [];
|
|
776
996
|
for (const component of request.components) {
|
|
777
|
-
const
|
|
997
|
+
const { memo } = this.verify(component.snapshot);
|
|
998
|
+
const result = await pageRequest.run({ path: memo.path, method: memo.method }, () => this.updateComponent(component.snapshot, component.updates, component.calls));
|
|
778
999
|
components.push({ snapshot: JSON.stringify(result.snapshot), effects: result.effects });
|
|
779
1000
|
}
|
|
780
1001
|
return { components };
|
|
@@ -787,22 +1008,24 @@ var LiveComponentManager = class {
|
|
|
787
1008
|
state.id = snapshot.memo.id;
|
|
788
1009
|
state.name = snapshot.memo.name;
|
|
789
1010
|
state.errors = { ...snapshot.memo.errors };
|
|
790
|
-
await
|
|
1011
|
+
await hook2(instance, "boot");
|
|
791
1012
|
const registry = this.registry();
|
|
792
1013
|
for (const [property, value] of Object.entries(snapshot.data)) {
|
|
793
1014
|
Object.defineProperty(instance, property, { value: await hydrate(value, registry), enumerable: true, writable: true, configurable: true });
|
|
794
1015
|
}
|
|
795
1016
|
this.prepareComputed(instance);
|
|
796
|
-
await hook(instance, "hydrate");
|
|
797
1017
|
const returns = [];
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
1018
|
+
await renderPaginated(instance, this.pageUrl(snapshot.memo.path), async () => {
|
|
1019
|
+
await hook2(instance, "hydrate");
|
|
1020
|
+
try {
|
|
1021
|
+
for (const [path, value] of Object.entries(updates)) await this.applyUpdate(instance, path, value);
|
|
1022
|
+
for (const call of calls) returns.push(await this.call(instance, call.method, call.params));
|
|
1023
|
+
} catch (error) {
|
|
1024
|
+
const errors = error.errors;
|
|
1025
|
+
if (error.kind !== "Validation" || !isRecord(errors)) throw error;
|
|
1026
|
+
state.errors = { ...errors };
|
|
1027
|
+
}
|
|
1028
|
+
});
|
|
806
1029
|
const memo = { ...snapshot.memo, children: { ...snapshot.memo.children } };
|
|
807
1030
|
const effects = this.effects(instance);
|
|
808
1031
|
let html;
|
|
@@ -816,15 +1039,37 @@ var LiveComponentManager = class {
|
|
|
816
1039
|
if (calls.length > 0) effects.returns = returns;
|
|
817
1040
|
return { instance, snapshot: await this.snapshot(instance, memo), effects, html, tag };
|
|
818
1041
|
}
|
|
819
|
-
/**
|
|
1042
|
+
/**
|
|
1043
|
+
* The route action of POST /oriel/live/update. Each component's update runs behind the persistent middleware of the
|
|
1044
|
+
* route that rendered its page (Livewire's): `auth`, `verified`, `guest` and `can`, with the page route's parameters
|
|
1045
|
+
* bound again. A signed-out tab gets a 401, a revoked ability a 403 and a deleted model a 404, before the component
|
|
1046
|
+
* hydrates. When a middleware answers instead (a redirect), that is the response. A key first flashed during the
|
|
1047
|
+
* update shows in this response only, unless the update redirects.
|
|
1048
|
+
*/
|
|
820
1049
|
async handle(request) {
|
|
821
1050
|
if (!this.isLiveRequest(request)) throw new InvalidLiveRequestError("Live update requests need the X-Oriel-Live header.");
|
|
822
|
-
|
|
1051
|
+
const container = this.container();
|
|
1052
|
+
const persistent = optional(container, ConfigRepository)?.get("live.persistent_middleware", PERSISTENT_MIDDLEWARE) ?? PERSISTENT_MIDDLEWARE;
|
|
1053
|
+
const components = [];
|
|
1054
|
+
const flashed = trackFlash(optional(container, Session2));
|
|
1055
|
+
for (const component of this.payload(request).components) {
|
|
1056
|
+
const { memo } = this.verify(component.snapshot);
|
|
1057
|
+
const pass = persistentPass(container.make(Router), persistent, memo.method, memo.path);
|
|
1058
|
+
const update = () => this.updateComponent(component.snapshot, component.updates, component.calls);
|
|
1059
|
+
const result = await pageRequest.run(
|
|
1060
|
+
{ path: memo.path, method: memo.method },
|
|
1061
|
+
() => pass === void 0 || pass.middleware.length === 0 ? update() : throughMiddleware(container, request, pass, update)
|
|
1062
|
+
);
|
|
1063
|
+
if (result instanceof Response) return result;
|
|
1064
|
+
components.push({ snapshot: JSON.stringify(result.snapshot), effects: result.effects });
|
|
1065
|
+
}
|
|
1066
|
+
flashed(components.some((component) => component.effects.redirect !== void 0));
|
|
1067
|
+
return { components };
|
|
823
1068
|
}
|
|
824
1069
|
/** A full-page component (Route.get('/counter', Counter)): mounted with the route parameters, inside the layout. */
|
|
825
1070
|
async page(component) {
|
|
826
1071
|
const container = this.container();
|
|
827
|
-
const params = { ...optional(container,
|
|
1072
|
+
const params = { ...optional(container, CurrentRoute2)?.parameters ?? {} };
|
|
828
1073
|
const cls = component.constructor;
|
|
829
1074
|
const result = await this.mountComponent(cls, params, component);
|
|
830
1075
|
const redirect = result.effects.redirect;
|
|
@@ -893,8 +1138,8 @@ var LiveComponentManager = class {
|
|
|
893
1138
|
const [root, ...rest] = path.split(".");
|
|
894
1139
|
const key = rest.length > 0 ? rest.join(".") : void 0;
|
|
895
1140
|
const studly = Str2.studly(root);
|
|
896
|
-
await
|
|
897
|
-
await
|
|
1141
|
+
await hook2(instance, "updating", path, value);
|
|
1142
|
+
await hook2(instance, `updating${studly}`, value, key);
|
|
898
1143
|
const target = instance;
|
|
899
1144
|
if (rest.length === 0) target[root] = value;
|
|
900
1145
|
else {
|
|
@@ -912,8 +1157,8 @@ var LiveComponentManager = class {
|
|
|
912
1157
|
assertSlot(node, last);
|
|
913
1158
|
Object.defineProperty(node, last, { value, enumerable: true, writable: true, configurable: true });
|
|
914
1159
|
}
|
|
915
|
-
await
|
|
916
|
-
await
|
|
1160
|
+
await hook2(instance, "updated", path, value);
|
|
1161
|
+
await hook2(instance, `updated${studly}`, value, key);
|
|
917
1162
|
}
|
|
918
1163
|
/** Runs one call from the browser: a magic action, an event, or a public method. Returns its return value. */
|
|
919
1164
|
async call(instance, method, params) {
|
|
@@ -941,18 +1186,35 @@ var LiveComponentManager = class {
|
|
|
941
1186
|
if (!isCallable(instance, method)) throw new MethodNotCallableError(method, name);
|
|
942
1187
|
return instance[method](...params);
|
|
943
1188
|
}
|
|
944
|
-
/**
|
|
1189
|
+
/**
|
|
1190
|
+
* Renders the component (rendering/rendered hooks, nested children tracked) and checks for a single root. A
|
|
1191
|
+
* WithPagination component's page and page URL are what paginate() reads while it renders, as in its mount() and in
|
|
1192
|
+
* its update's hooks and actions.
|
|
1193
|
+
*/
|
|
945
1194
|
async render(instance, memo, previous) {
|
|
946
|
-
await
|
|
1195
|
+
await hook2(instance, "rendering");
|
|
947
1196
|
const context = { previous, children: {}, index: 0 };
|
|
948
|
-
const html = await renderContext.run(
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
1197
|
+
const html = await renderContext.run(
|
|
1198
|
+
context,
|
|
1199
|
+
() => renderPaginated(instance, this.pageUrl(memo.path), async () => {
|
|
1200
|
+
const output = await instance.render();
|
|
1201
|
+
return typeof output === "string" ? output : this.container().make(ViewFactory).renderToString(output);
|
|
1202
|
+
})
|
|
1203
|
+
);
|
|
1204
|
+
await hook2(instance, "rendered", html);
|
|
953
1205
|
memo.children = context.children;
|
|
954
1206
|
return { html: html.trim(), tag: rootTag(html, memo.name) };
|
|
955
1207
|
}
|
|
1208
|
+
/** The absolute URL of the page a memo path belongs to, on the current request's origin ('/' + path without one). */
|
|
1209
|
+
pageUrl(path) {
|
|
1210
|
+
const relative3 = `/${path.replace(/^\/+/, "")}`;
|
|
1211
|
+
const url = optional(this.container(), HttpRequest)?.url();
|
|
1212
|
+
try {
|
|
1213
|
+
return url === void 0 ? relative3 : new URL(relative3, url).href;
|
|
1214
|
+
} catch {
|
|
1215
|
+
return relative3;
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
956
1218
|
/** Dispatches, redirects: the effects every response carries. */
|
|
957
1219
|
effects(instance) {
|
|
958
1220
|
const state = stateOf(instance);
|
|
@@ -971,7 +1233,7 @@ var LiveComponentManager = class {
|
|
|
971
1233
|
}
|
|
972
1234
|
/** dehydrate() hook, then the state as snapshot data, the error bag, and the checksum over both. */
|
|
973
1235
|
async snapshot(instance, memo) {
|
|
974
|
-
await
|
|
1236
|
+
await hook2(instance, "dehydrate");
|
|
975
1237
|
const registry = this.registry();
|
|
976
1238
|
const data = {};
|
|
977
1239
|
for (const [property, value] of Object.entries(instance)) data[property] = dehydrate(value, registry, `${memo.name}.${property}`);
|
|
@@ -981,7 +1243,7 @@ var LiveComponentManager = class {
|
|
|
981
1243
|
};
|
|
982
1244
|
|
|
983
1245
|
// LiveServiceProvider.ts
|
|
984
|
-
import { ConfigRepository as ConfigRepository3, ConsoleKernel, HttpRequest as HttpRequest2, LiveManager as LiveManager4, Router } from "@orieljs/contracts";
|
|
1246
|
+
import { ConfigRepository as ConfigRepository3, ConsoleKernel, HttpRequest as HttpRequest2, LiveManager as LiveManager4, PaginationState, Router as Router2 } from "@orieljs/contracts";
|
|
985
1247
|
import { inject as inject4 } from "@orieljs/container";
|
|
986
1248
|
import { ServiceProvider } from "@orieljs/support";
|
|
987
1249
|
|
|
@@ -1055,14 +1317,15 @@ function registerLiveCommands(kernel, app) {
|
|
|
1055
1317
|
var UPDATE_URI = "/oriel/live/update";
|
|
1056
1318
|
var LiveServiceProvider = class extends ServiceProvider {
|
|
1057
1319
|
register() {
|
|
1058
|
-
this.mergeConfigFrom({ class_path: this.app.appPath("Live"), layout: "components.layouts.app" }, "live");
|
|
1320
|
+
this.mergeConfigFrom({ class_path: this.app.appPath("Live"), layout: "components.layouts.app", persistent_middleware: [...PERSISTENT_MIDDLEWARE] }, "live");
|
|
1059
1321
|
this.app.singleton(LiveComponentManager, () => new LiveComponentManager(this.app));
|
|
1060
1322
|
this.app.singleton(LiveManager4, (container) => container.make(LiveComponentManager));
|
|
1323
|
+
this.app.singleton(PaginationState, () => livePaginationState);
|
|
1061
1324
|
}
|
|
1062
1325
|
async boot() {
|
|
1063
1326
|
const manager = this.app.make(LiveComponentManager);
|
|
1064
|
-
if (this.app.bound(
|
|
1065
|
-
this.app.make(
|
|
1327
|
+
if (this.app.bound(Router2)) {
|
|
1328
|
+
this.app.make(Router2).post(UPDATE_URI, () => manager.handle(inject4(HttpRequest2))).middleware("web").name("live.update");
|
|
1066
1329
|
}
|
|
1067
1330
|
if (this.app.bound(ConsoleKernel)) registerLiveCommands(this.app.make(ConsoleKernel), this.app);
|
|
1068
1331
|
const directory = this.app.make(ConfigRepository3).get("live.class_path", this.app.appPath("Live"));
|
|
@@ -1071,6 +1334,42 @@ var LiveServiceProvider = class extends ServiceProvider {
|
|
|
1071
1334
|
}
|
|
1072
1335
|
}
|
|
1073
1336
|
};
|
|
1337
|
+
|
|
1338
|
+
// Pagination.ts
|
|
1339
|
+
function urlWindow(current, last, onEachSide) {
|
|
1340
|
+
const range = (from, to) => Array.from({ length: Math.max(to - from + 1, 0) }, (_, index) => from + index);
|
|
1341
|
+
if (last < onEachSide * 2 + 8) return range(1, last);
|
|
1342
|
+
const size = onEachSide + 4;
|
|
1343
|
+
if (current <= size) return [...range(1, size + onEachSide), "...", ...range(last - 1, last)];
|
|
1344
|
+
if (current > last - size) return [...range(1, 2), "...", ...range(last - (size + (onEachSide - 1)), last)];
|
|
1345
|
+
return [...range(1, 2), "...", ...range(current - onEachSide, current + onEachSide), "...", ...range(last - 1, last)];
|
|
1346
|
+
}
|
|
1347
|
+
function button(label, action, attributes = "") {
|
|
1348
|
+
const live2 = action === void 0 ? " disabled" : ` o:click="${escapeAttribute(action)}"`;
|
|
1349
|
+
return `<button type="button" class="btn btn-secondary"${attributes}${live2}>${escapeAttribute(label)}</button>`;
|
|
1350
|
+
}
|
|
1351
|
+
function Pagination({ paginator, class: className }) {
|
|
1352
|
+
if (typeof paginator.nextCursor === "function") {
|
|
1353
|
+
throw new LiveError("<Pagination> needs paginate() or simplePaginate(): a cursor paginator has no page numbers.");
|
|
1354
|
+
}
|
|
1355
|
+
const current = paginator.currentPage();
|
|
1356
|
+
const onFirst = paginator.onFirstPage();
|
|
1357
|
+
const hasMore = paginator.hasMorePages();
|
|
1358
|
+
if (onFirst && !hasMore) return { toHtml: () => "" };
|
|
1359
|
+
const parts = [button("\xAB Previous", onFirst ? void 0 : "previousPage")];
|
|
1360
|
+
if (typeof paginator.lastPage === "function") {
|
|
1361
|
+
const onEachSide = Number.isInteger(paginator.onEachSideCount) && paginator.onEachSideCount >= 0 ? paginator.onEachSideCount : 3;
|
|
1362
|
+
for (const page of urlWindow(current, paginator.lastPage(), onEachSide)) {
|
|
1363
|
+
if (page === "...") parts.push('<span aria-hidden="true">...</span>');
|
|
1364
|
+
else if (page === current) parts.push(`<button type="button" class="btn" aria-current="page" disabled>${page}</button>`);
|
|
1365
|
+
else parts.push(button(String(page), `gotoPage(${page})`, ` aria-label="Go to page ${page}"`));
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
parts.push(button("Next \xBB", hasMore ? "nextPage" : void 0));
|
|
1369
|
+
const classes = className === void 0 || className === "" ? "" : ` class="${escapeAttribute(className)}"`;
|
|
1370
|
+
const html = `<nav aria-label="Pagination"${classes}>${parts.join("")}</nav>`;
|
|
1371
|
+
return { toHtml: () => html };
|
|
1372
|
+
}
|
|
1074
1373
|
export {
|
|
1075
1374
|
Component,
|
|
1076
1375
|
Computed,
|
|
@@ -1085,11 +1384,16 @@ export {
|
|
|
1085
1384
|
Locked,
|
|
1086
1385
|
MethodNotCallableError,
|
|
1087
1386
|
On,
|
|
1387
|
+
PERSISTENT_MIDDLEWARE,
|
|
1388
|
+
Pagination,
|
|
1088
1389
|
PendingEvent,
|
|
1089
1390
|
PropertyNotUpdatableError,
|
|
1391
|
+
TestRequestScope,
|
|
1090
1392
|
Testable,
|
|
1091
1393
|
UPDATE_URI,
|
|
1394
|
+
WithPagination,
|
|
1092
1395
|
componentName,
|
|
1093
|
-
live
|
|
1396
|
+
live,
|
|
1397
|
+
persistentMiddleware
|
|
1094
1398
|
};
|
|
1095
1399
|
//# sourceMappingURL=index.js.map
|