@orieljs/live 0.1.0-alpha.4 → 0.1.0-alpha.6
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 +3 -0
- package/README.md +100 -1
- package/dist/index.d.ts +76 -10
- package/dist/index.js +457 -226
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
package/dist/index.js
CHANGED
|
@@ -2,6 +2,50 @@
|
|
|
2
2
|
import { LiveManager, UrlGenerator, ValidationFactory } from "@orieljs/contracts";
|
|
3
3
|
import { inject } from "@orieljs/container";
|
|
4
4
|
|
|
5
|
+
// errors.ts
|
|
6
|
+
var LiveError = class extends Error {
|
|
7
|
+
name = "LiveError";
|
|
8
|
+
kind = "Server";
|
|
9
|
+
status = 500;
|
|
10
|
+
};
|
|
11
|
+
var CorruptComponentPayloadError = class extends LiveError {
|
|
12
|
+
name = "CorruptComponentPayloadError";
|
|
13
|
+
kind = "Authorization";
|
|
14
|
+
status = 403;
|
|
15
|
+
constructor(component = "unknown") {
|
|
16
|
+
super(`Live encountered corrupt data when trying to hydrate the [${component}] component.`);
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
var MethodNotCallableError = class extends LiveError {
|
|
20
|
+
name = "MethodNotCallableError";
|
|
21
|
+
kind = "Authorization";
|
|
22
|
+
status = 403;
|
|
23
|
+
constructor(method, component) {
|
|
24
|
+
super(`Unable to call component method. Public method [${method}] not found on component [${component}].`);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
var PropertyNotUpdatableError = class extends LiveError {
|
|
28
|
+
name = "PropertyNotUpdatableError";
|
|
29
|
+
kind = "Authorization";
|
|
30
|
+
status = 403;
|
|
31
|
+
constructor(path, component) {
|
|
32
|
+
super(`Cannot update property [${path}] on component [${component}]: it is not a public, unlocked property.`);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
var LiveComponentNotFoundError = class extends LiveError {
|
|
36
|
+
name = "LiveComponentNotFoundError";
|
|
37
|
+
kind = "Http";
|
|
38
|
+
status = 404;
|
|
39
|
+
constructor(component) {
|
|
40
|
+
super(`Unable to find component: [${component}].`);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
var InvalidLiveRequestError = class extends LiveError {
|
|
44
|
+
name = "InvalidLiveRequestError";
|
|
45
|
+
kind = "Http";
|
|
46
|
+
status = 400;
|
|
47
|
+
};
|
|
48
|
+
|
|
5
49
|
// metadata.ts
|
|
6
50
|
Symbol.metadata ??= /* @__PURE__ */ Symbol.for("Symbol.metadata");
|
|
7
51
|
var LISTENERS = /* @__PURE__ */ Symbol.for("oriel:live.listeners");
|
|
@@ -75,6 +119,153 @@ function computedOf(cls) {
|
|
|
75
119
|
return /* @__PURE__ */ new Set([...metadataOf(cls)[COMPUTED] ?? [], ...staticsOf(cls, "computed").flat()]);
|
|
76
120
|
}
|
|
77
121
|
|
|
122
|
+
// security.ts
|
|
123
|
+
import { Collection as Collection2 } from "@orieljs/collections";
|
|
124
|
+
|
|
125
|
+
// synthesizers.ts
|
|
126
|
+
import { Collection } from "@orieljs/collections";
|
|
127
|
+
function isModel(value) {
|
|
128
|
+
if (typeof value !== "object" || value === null) return false;
|
|
129
|
+
const candidate = value;
|
|
130
|
+
return typeof candidate.getKey === "function" && typeof candidate.getKeyName === "function" && typeof candidate.getTable === "function" && typeof candidate.exists === "boolean";
|
|
131
|
+
}
|
|
132
|
+
var isPlainObject = (value) => {
|
|
133
|
+
const proto = Object.getPrototypeOf(value);
|
|
134
|
+
return proto === Object.prototype || proto === null;
|
|
135
|
+
};
|
|
136
|
+
var isTuple = (value) => Array.isArray(value) && value.length === 2 && typeof value[1] === "object" && value[1] !== null && !Array.isArray(value[1]) && typeof value[1].s === "string";
|
|
137
|
+
function classNameOf(registry, cls) {
|
|
138
|
+
return registry.nameOf(cls) ?? registry.register(cls);
|
|
139
|
+
}
|
|
140
|
+
function dehydrate(value, registry, path, seen = /* @__PURE__ */ new Set()) {
|
|
141
|
+
switch (typeof value) {
|
|
142
|
+
case "undefined":
|
|
143
|
+
return [null, { s: "u" }];
|
|
144
|
+
case "string":
|
|
145
|
+
case "boolean":
|
|
146
|
+
return value;
|
|
147
|
+
case "number":
|
|
148
|
+
if (!Number.isFinite(value)) throw new LiveError(`Property [${path}] is ${value}: Live state must be JSON (finite numbers only).`);
|
|
149
|
+
return value;
|
|
150
|
+
case "bigint":
|
|
151
|
+
case "symbol":
|
|
152
|
+
case "function":
|
|
153
|
+
throw new LiveError(`Property [${path}] holds a ${typeof value}: Live state may only hold primitives, arrays, plain objects, Dates, models and collections.`);
|
|
154
|
+
}
|
|
155
|
+
if (value === null) return null;
|
|
156
|
+
const object = value;
|
|
157
|
+
if (seen.has(object)) throw new LiveError(`Property [${path}] contains a circular reference.`);
|
|
158
|
+
seen.add(object);
|
|
159
|
+
try {
|
|
160
|
+
if (object instanceof Date) return [object.toISOString(), { s: "dt" }];
|
|
161
|
+
if (Array.isArray(object)) return [object.map((item, i) => dehydrate(item, registry, `${path}.${i}`, seen)), { s: "arr" }];
|
|
162
|
+
if (isModel(object)) {
|
|
163
|
+
const name = classNameOf(registry, object.constructor);
|
|
164
|
+
if (!object.exists) throw new LiveError(`Property [${path}] holds an unsaved [${name}] model: only saved models can be kept between requests.`);
|
|
165
|
+
return [null, { s: "mdl", class: name, key: object.getKey() }];
|
|
166
|
+
}
|
|
167
|
+
if (object instanceof Collection) {
|
|
168
|
+
const items = object.all();
|
|
169
|
+
const first = items[0];
|
|
170
|
+
if (items.length > 0 && isModel(first) && items.every((item) => isModel(item) && item.constructor === first.constructor)) {
|
|
171
|
+
for (const [i, item] of items.entries()) {
|
|
172
|
+
if (!item.exists) throw new LiveError(`Property [${path}.${i}] holds an unsaved model.`);
|
|
173
|
+
}
|
|
174
|
+
const name = classNameOf(registry, first.constructor);
|
|
175
|
+
return [null, { s: "elcln", class: name, keys: items.map((item) => item.getKey()) }];
|
|
176
|
+
}
|
|
177
|
+
return [items.map((item, i) => dehydrate(item, registry, `${path}.${i}`, seen)), { s: "clctn" }];
|
|
178
|
+
}
|
|
179
|
+
if (isPlainObject(object)) {
|
|
180
|
+
const out = {};
|
|
181
|
+
for (const [key, item] of Object.entries(object)) out[key] = dehydrate(item, registry, `${path}.${key}`, seen);
|
|
182
|
+
return [out, { s: "obj" }];
|
|
183
|
+
}
|
|
184
|
+
const type = object.constructor?.name || "object";
|
|
185
|
+
throw new LiveError(
|
|
186
|
+
`Property [${path}] holds a [${type}] instance: Live state may only hold primitives, arrays, plain objects, Dates, models and collections.`
|
|
187
|
+
);
|
|
188
|
+
} finally {
|
|
189
|
+
seen.delete(object);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
function modelClass(registry, name) {
|
|
193
|
+
if (typeof name !== "string" || !registry.has(name)) throw new LiveError(`Unable to hydrate the [${String(name)}] model: it is not registered.`);
|
|
194
|
+
return registry.resolve(name);
|
|
195
|
+
}
|
|
196
|
+
async function hydrate(value, registry) {
|
|
197
|
+
if (!isTuple(value)) return value;
|
|
198
|
+
const [inner, meta] = value;
|
|
199
|
+
switch (meta.s) {
|
|
200
|
+
case "u":
|
|
201
|
+
return void 0;
|
|
202
|
+
case "dt":
|
|
203
|
+
return new Date(String(inner));
|
|
204
|
+
case "arr":
|
|
205
|
+
return Promise.all(inner.map((item) => hydrate(item, registry)));
|
|
206
|
+
case "clctn":
|
|
207
|
+
return new Collection(await Promise.all(inner.map((item) => hydrate(item, registry))));
|
|
208
|
+
case "obj": {
|
|
209
|
+
const out = {};
|
|
210
|
+
for (const [key, item] of Object.entries(inner)) {
|
|
211
|
+
Object.defineProperty(out, key, { value: await hydrate(item, registry), enumerable: true, writable: true, configurable: true });
|
|
212
|
+
}
|
|
213
|
+
return out;
|
|
214
|
+
}
|
|
215
|
+
case "mdl":
|
|
216
|
+
return modelClass(registry, meta.class).findOrFail(meta.key);
|
|
217
|
+
case "elcln": {
|
|
218
|
+
const cls = modelClass(registry, meta.class);
|
|
219
|
+
const keys = meta.keys ?? [];
|
|
220
|
+
const keyName = new cls().getKeyName();
|
|
221
|
+
const found = [...await cls.query().whereIn(keyName, keys).get()];
|
|
222
|
+
const byKey = new Map(found.map((model) => [String(model.getKey()), model]));
|
|
223
|
+
return new Collection(keys.map((key) => byKey.get(String(key))).filter((model) => model !== void 0));
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
throw new LiveError(`Unknown snapshot value type [${String(meta.s)}].`);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// security.ts
|
|
230
|
+
var HOOKS = /* @__PURE__ */ new Set(["constructor", "render", "mount", "boot", "booted", "hydrate", "dehydrate", "updating", "updated", "rendering", "rendered", "exception"]);
|
|
231
|
+
var HOOK_PREFIX = /^(updating|updated|hydrate|dehydrate)[A-Z_]/;
|
|
232
|
+
var UNSAFE_SEGMENTS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
233
|
+
var baseApi;
|
|
234
|
+
var isBaseApi = (name) => (baseApi ??= new Set(Object.getOwnPropertyNames(Component.prototype))).has(name);
|
|
235
|
+
var INDEX = /^(?:0|[1-9]\d*)$/;
|
|
236
|
+
function isArraySlot(array, segment) {
|
|
237
|
+
return INDEX.test(segment) && Number(segment) <= array.length;
|
|
238
|
+
}
|
|
239
|
+
function isCallable(instance, method) {
|
|
240
|
+
if (method === "" || method.startsWith("_") || method.startsWith("$") || method.startsWith("#")) return false;
|
|
241
|
+
if (HOOKS.has(method) || HOOK_PREFIX.test(method) || isBaseApi(method)) return false;
|
|
242
|
+
if (lockedOf(instance.constructor).has(method)) return false;
|
|
243
|
+
const isBase = (proto) => proto === Component.prototype || proto === Object.prototype;
|
|
244
|
+
for (let proto = Object.getPrototypeOf(instance); proto !== null && !isBase(proto); proto = Object.getPrototypeOf(proto)) {
|
|
245
|
+
const descriptor = Object.getOwnPropertyDescriptor(proto, method);
|
|
246
|
+
if (descriptor !== void 0) return typeof descriptor.value === "function";
|
|
247
|
+
}
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
function isUpdatable(instance, path) {
|
|
251
|
+
const segments = path.split(".");
|
|
252
|
+
const root = segments[0];
|
|
253
|
+
if (root === "" || root.startsWith("_") || segments.some((segment) => segment === "" || UNSAFE_SEGMENTS.has(segment))) return false;
|
|
254
|
+
if (!Object.hasOwn(instance, root)) return false;
|
|
255
|
+
const descriptor = Object.getOwnPropertyDescriptor(instance, root);
|
|
256
|
+
if (!descriptor.enumerable || descriptor.get !== void 0) return false;
|
|
257
|
+
const cls = instance.constructor;
|
|
258
|
+
if (lockedOf(cls).has(root) || computedOf(cls).has(root)) return false;
|
|
259
|
+
let node = descriptor.value;
|
|
260
|
+
for (const segment of segments.slice(1)) {
|
|
261
|
+
if (isModel(node) || node instanceof Collection2) return false;
|
|
262
|
+
if (Array.isArray(node) && !isArraySlot(node, segment)) return false;
|
|
263
|
+
if (typeof node !== "object" || node === null || !Object.hasOwn(node, segment)) return true;
|
|
264
|
+
node = node[segment];
|
|
265
|
+
}
|
|
266
|
+
return !isModel(node) && !(node instanceof Collection2);
|
|
267
|
+
}
|
|
268
|
+
|
|
78
269
|
// Component.ts
|
|
79
270
|
var COMPONENT = /* @__PURE__ */ Symbol.for("oriel:live.component");
|
|
80
271
|
var states = /* @__PURE__ */ new WeakMap();
|
|
@@ -206,6 +397,23 @@ var Component = class {
|
|
|
206
397
|
skipRender() {
|
|
207
398
|
stateOf(this).skipRender = true;
|
|
208
399
|
}
|
|
400
|
+
/**
|
|
401
|
+
* An action for an `o:` directive, typed against its method: `o:click={this.$action(this.delete, tag.id)}` writes
|
|
402
|
+
* `delete(5)`, and a string where `delete` takes a number is a type error. The arguments are written as JSON, the
|
|
403
|
+
* trailing `undefined` ones left out so the method's defaults apply. The method must be one of this component's
|
|
404
|
+
* actions (a public method of its class, not a hook and not @Locked); anything else throws while rendering.
|
|
405
|
+
*/
|
|
406
|
+
$action(method, ...params) {
|
|
407
|
+
const name = typeof method === "function" ? method.name : "";
|
|
408
|
+
if (name === "" || this[name] !== method || !isCallable(this, name)) {
|
|
409
|
+
throw new LiveError(
|
|
410
|
+
`[${name === "" ? "anonymous function" : name}] is not an action of [${this.getName() || this.constructor.name}]: pass one of its public methods, as in this.$action(this.save).`
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
let count = params.length;
|
|
414
|
+
while (count > 0 && params[count - 1] === void 0) count--;
|
|
415
|
+
return count === 0 ? name : `${name}(${params.slice(0, count).map((param) => JSON.stringify(param) ?? "null").join(", ")})`;
|
|
416
|
+
}
|
|
209
417
|
/** A full-page component as a route action: Route.get('/counter', Counter). Route parameters go to mount(). */
|
|
210
418
|
async __invoke() {
|
|
211
419
|
return inject(LiveManager).page(this);
|
|
@@ -219,58 +427,33 @@ import { inject as inject3 } from "@orieljs/container";
|
|
|
219
427
|
// Testable.ts
|
|
220
428
|
import { AssertionError } from "assert";
|
|
221
429
|
import { isDeepStrictEqual } from "util";
|
|
222
|
-
import { LiveManager as LiveManager2, token } from "@orieljs/contracts";
|
|
430
|
+
import { LiveManager as LiveManager2, Session, token } from "@orieljs/contracts";
|
|
223
431
|
import { Container, currentContainer, inject as inject2 } from "@orieljs/container";
|
|
224
432
|
|
|
225
|
-
//
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
};
|
|
239
|
-
|
|
240
|
-
name = "MethodNotCallableError";
|
|
241
|
-
kind = "Authorization";
|
|
242
|
-
status = 403;
|
|
243
|
-
constructor(method, component) {
|
|
244
|
-
super(`Unable to call component method. Public method [${method}] not found on component [${component}].`);
|
|
245
|
-
}
|
|
246
|
-
};
|
|
247
|
-
var PropertyNotUpdatableError = class extends LiveError {
|
|
248
|
-
name = "PropertyNotUpdatableError";
|
|
249
|
-
kind = "Authorization";
|
|
250
|
-
status = 403;
|
|
251
|
-
constructor(path, component) {
|
|
252
|
-
super(`Cannot update property [${path}] on component [${component}]: it is not a public, unlocked property.`);
|
|
253
|
-
}
|
|
254
|
-
};
|
|
255
|
-
var LiveComponentNotFoundError = class extends LiveError {
|
|
256
|
-
name = "LiveComponentNotFoundError";
|
|
257
|
-
kind = "Http";
|
|
258
|
-
status = 404;
|
|
259
|
-
constructor(component) {
|
|
260
|
-
super(`Unable to find component: [${component}].`);
|
|
261
|
-
}
|
|
262
|
-
};
|
|
263
|
-
var InvalidLiveRequestError = class extends LiveError {
|
|
264
|
-
name = "InvalidLiveRequestError";
|
|
265
|
-
kind = "Http";
|
|
266
|
-
status = 400;
|
|
267
|
-
};
|
|
433
|
+
// flash.ts
|
|
434
|
+
function trackFlash(session, { saved = true } = {}) {
|
|
435
|
+
if (session === void 0) return () => void 0;
|
|
436
|
+
const before = new Set(session.get("_flash.new", []));
|
|
437
|
+
return (redirected) => {
|
|
438
|
+
if (redirected) return;
|
|
439
|
+
const flashed = session.get("_flash.new", []);
|
|
440
|
+
const fresh = flashed.filter((key) => !before.has(key));
|
|
441
|
+
if (fresh.length === 0) return;
|
|
442
|
+
session.put("_flash.new", flashed.filter((key) => before.has(key)));
|
|
443
|
+
if (!saved) return session.forget(...fresh);
|
|
444
|
+
const old = session.get("_flash.old", []);
|
|
445
|
+
session.put("_flash.old", [...old, ...fresh.filter((key) => !old.includes(key))]);
|
|
446
|
+
};
|
|
447
|
+
}
|
|
268
448
|
|
|
269
449
|
// html.ts
|
|
270
450
|
var ESCAPES = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" };
|
|
271
451
|
function escapeAttribute(value) {
|
|
272
452
|
return value.replace(/[&<>"']/g, (char) => ESCAPES[char]);
|
|
273
453
|
}
|
|
454
|
+
function stripTags(html) {
|
|
455
|
+
return html.replace(/<[^>]*>/g, "");
|
|
456
|
+
}
|
|
274
457
|
var VOID = /* @__PURE__ */ new Set(["area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "source", "track", "wbr"]);
|
|
275
458
|
var RAW_TEXT = /* @__PURE__ */ new Set(["script", "style", "textarea", "title"]);
|
|
276
459
|
var TOKEN = /<\/([A-Za-z][\w:-]*)\s*>|<([A-Za-z][\w:-]*)(?:\s+[^\s"'<>/=]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s"'=<>`]+))?)*\s*(\/?)>/g;
|
|
@@ -327,6 +510,15 @@ function requestScope() {
|
|
|
327
510
|
const container = currentContainer() ?? Container.getInstance();
|
|
328
511
|
return container?.bound(TestRequestScope) === true ? container.make(TestRequestScope) : (run) => run();
|
|
329
512
|
}
|
|
513
|
+
function currentSession() {
|
|
514
|
+
const container = currentContainer();
|
|
515
|
+
if (container?.bound(Session) !== true) return void 0;
|
|
516
|
+
try {
|
|
517
|
+
return container.make(Session);
|
|
518
|
+
} catch {
|
|
519
|
+
return void 0;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
330
522
|
var Testable = class _Testable {
|
|
331
523
|
constructor(manager, scope, result) {
|
|
332
524
|
this.manager = manager;
|
|
@@ -381,7 +573,12 @@ var Testable = class _Testable {
|
|
|
381
573
|
return this.#update({}, [{ method: "$refresh", params: [] }]);
|
|
382
574
|
}
|
|
383
575
|
async #update(updates, calls) {
|
|
384
|
-
const result = await this.scope(() =>
|
|
576
|
+
const result = await this.scope(async () => {
|
|
577
|
+
const flashed = trackFlash(currentSession(), { saved: false });
|
|
578
|
+
const updated = await this.manager.updateComponent(this.#snapshot, updates, calls);
|
|
579
|
+
flashed(updated.effects.redirect !== void 0);
|
|
580
|
+
return updated;
|
|
581
|
+
});
|
|
385
582
|
this.#snapshot = JSON.stringify(result.snapshot);
|
|
386
583
|
if (result.html !== void 0) this.#html = result.html;
|
|
387
584
|
this.#effects = result.effects;
|
|
@@ -411,6 +608,17 @@ var Testable = class _Testable {
|
|
|
411
608
|
for (const value of values) if (!this.html().includes(value)) fail(`Failed asserting that [${value}] is in the rendered HTML.`, this.html(), value);
|
|
412
609
|
return this;
|
|
413
610
|
}
|
|
611
|
+
/** Each value, HTML-escaped, appears in the rendered text: the HTML with its tags stripped (Livewire's assertSeeText). */
|
|
612
|
+
assertSeeText(...values) {
|
|
613
|
+
const text = stripTags(this.html());
|
|
614
|
+
for (const value of values) if (!text.includes(escapeAttribute(value))) fail(`Failed asserting that [${value}] is in the rendered text.`, text, value);
|
|
615
|
+
return this;
|
|
616
|
+
}
|
|
617
|
+
assertDontSeeText(...values) {
|
|
618
|
+
const text = stripTags(this.html());
|
|
619
|
+
for (const value of values) if (text.includes(escapeAttribute(value))) fail(`Failed asserting that [${value}] is not in the rendered text.`, text, value);
|
|
620
|
+
return this;
|
|
621
|
+
}
|
|
414
622
|
assertDispatched(name, params) {
|
|
415
623
|
const dispatched = (this.#effects.dispatches ?? []).filter((event) => event.name === name);
|
|
416
624
|
if (dispatched.length === 0) fail(`Failed asserting that an event [${name}] was dispatched.`, this.#effects.dispatches ?? [], name);
|
|
@@ -477,16 +685,17 @@ var Live = Object.assign(LiveComponent, {
|
|
|
477
685
|
});
|
|
478
686
|
|
|
479
687
|
// LiveComponentManager.ts
|
|
480
|
-
import { AsyncLocalStorage } from "async_hooks";
|
|
688
|
+
import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
|
|
481
689
|
import { randomBytes } from "crypto";
|
|
482
690
|
import {
|
|
483
691
|
ClassRegistry,
|
|
484
692
|
ConfigRepository,
|
|
485
|
-
CurrentRoute,
|
|
693
|
+
CurrentRoute as CurrentRoute2,
|
|
486
694
|
Encrypter,
|
|
487
695
|
HttpRequest,
|
|
488
696
|
Redirector,
|
|
489
697
|
Router,
|
|
698
|
+
Session as Session2,
|
|
490
699
|
ViewFactory
|
|
491
700
|
} from "@orieljs/contracts";
|
|
492
701
|
import { currentContainer as currentContainer2 } from "@orieljs/container";
|
|
@@ -541,10 +750,12 @@ async function discover(directory) {
|
|
|
541
750
|
}
|
|
542
751
|
|
|
543
752
|
// persistent.ts
|
|
544
|
-
|
|
753
|
+
import { CurrentRoute } from "@orieljs/contracts";
|
|
754
|
+
var PERSISTENT_MIDDLEWARE = ["auth", "verified", "guest", "can"];
|
|
755
|
+
var ROUTER_METHODS = ["getRoutesByMatchOrder", "getPatterns", "getPatternVersion", "getMiddleware", "getMiddlewareGroups", "substituteBindings"];
|
|
545
756
|
function canMatch(router) {
|
|
546
757
|
const methods = router;
|
|
547
|
-
return
|
|
758
|
+
return ROUTER_METHODS.every((name) => typeof methods[name] === "function");
|
|
548
759
|
}
|
|
549
760
|
function isMatchingRoute(route) {
|
|
550
761
|
const candidate = route;
|
|
@@ -573,7 +784,7 @@ function expand(references, aliases, groups, out = [], visiting = /* @__PURE__ *
|
|
|
573
784
|
}
|
|
574
785
|
return out;
|
|
575
786
|
}
|
|
576
|
-
function
|
|
787
|
+
function persistentPass(router, persistent, method, path) {
|
|
577
788
|
if (!canMatch(router)) throw new LiveError("Live re-applies the page route's middleware to updates, which needs Oriel's router.");
|
|
578
789
|
const routes = router.getRoutesByMatchOrder();
|
|
579
790
|
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.");
|
|
@@ -581,187 +792,144 @@ function persistentMiddleware(router, persistent, method, path) {
|
|
|
581
792
|
const target = routePath(path);
|
|
582
793
|
const patterns = router.getPatterns();
|
|
583
794
|
const version = router.getPatternVersion();
|
|
584
|
-
|
|
585
|
-
|
|
795
|
+
let matched;
|
|
796
|
+
for (const candidate of routes) {
|
|
797
|
+
const values2 = candidate.methods.includes(verb) ? candidate.match(target, patterns, version) : void 0;
|
|
798
|
+
if (values2 !== void 0) {
|
|
799
|
+
matched = { route: candidate, values: values2 };
|
|
800
|
+
break;
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
if (matched === void 0) return void 0;
|
|
804
|
+
const { route, values } = matched;
|
|
586
805
|
const aliases = router.getMiddleware();
|
|
587
806
|
const groups = router.getMiddlewareGroups();
|
|
588
807
|
const wanted = new Set(persistent.flatMap((name) => Object.hasOwn(aliases, name) ? [aliases[name]] : []));
|
|
589
808
|
const excluded = new Set(expand(route.excludedMiddleware(), aliases, groups).map((entry) => entry.cls));
|
|
590
809
|
const ids = /* @__PURE__ */ new Map();
|
|
591
810
|
const seen = /* @__PURE__ */ new Set();
|
|
592
|
-
|
|
811
|
+
const middleware = expand(route.gatherMiddleware(), aliases, groups).filter((entry) => {
|
|
593
812
|
if (!ids.has(entry.cls)) ids.set(entry.cls, ids.size);
|
|
594
813
|
const key = `${ids.get(entry.cls)}:${entry.parameters.join(",")}`;
|
|
595
814
|
if (!wanted.has(entry.cls) || excluded.has(entry.cls) || seen.has(key)) return false;
|
|
596
815
|
seen.add(key);
|
|
597
816
|
return true;
|
|
598
817
|
});
|
|
818
|
+
const early = new Set(expand(["web", "api"], aliases, groups).map((entry) => entry.cls));
|
|
819
|
+
for (const name of ["auth", "guest"]) if (Object.hasOwn(aliases, name)) early.add(aliases[name]);
|
|
820
|
+
const before = middleware.filter((entry) => early.has(entry.cls));
|
|
821
|
+
const after = middleware.filter((entry) => !early.has(entry.cls));
|
|
822
|
+
return { router, route, values, middleware: [...before, ...after], at: before.length };
|
|
599
823
|
}
|
|
600
|
-
|
|
824
|
+
function persistentMiddleware(router, persistent, method, path) {
|
|
825
|
+
return [...persistentPass(router, persistent, method, path)?.middleware ?? []];
|
|
826
|
+
}
|
|
827
|
+
function currentRoute(container) {
|
|
828
|
+
if (!container.bound(CurrentRoute)) return void 0;
|
|
829
|
+
try {
|
|
830
|
+
return container.make(CurrentRoute);
|
|
831
|
+
} catch {
|
|
832
|
+
return void 0;
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
async function throughMiddleware(container, request, pass, destination) {
|
|
836
|
+
const original = currentRoute(container);
|
|
837
|
+
const page = { route: pass.route, parameters: { ...pass.values }, middleware: [] };
|
|
838
|
+
let swapped = false;
|
|
839
|
+
const show = () => {
|
|
840
|
+
container.instance(CurrentRoute, page);
|
|
841
|
+
swapped = true;
|
|
842
|
+
};
|
|
843
|
+
const restore = () => {
|
|
844
|
+
if (!swapped) return;
|
|
845
|
+
if (original === void 0) container.forgetInstance(CurrentRoute);
|
|
846
|
+
else container.instance(CurrentRoute, original);
|
|
847
|
+
swapped = false;
|
|
848
|
+
};
|
|
601
849
|
let ran;
|
|
602
850
|
const run = async (index) => {
|
|
603
|
-
if (index ===
|
|
851
|
+
if (index === pass.at) Object.assign(page.parameters, await pass.router.substituteBindings(pass.route, pass.values, container));
|
|
852
|
+
if (index === pass.middleware.length) {
|
|
853
|
+
restore();
|
|
604
854
|
ran = { result: await destination() };
|
|
605
855
|
return new Response(null, { status: 204 });
|
|
606
856
|
}
|
|
607
|
-
const { cls, parameters } = middleware[index];
|
|
857
|
+
const { cls, parameters } = pass.middleware[index];
|
|
608
858
|
const instance = container.make(cls);
|
|
609
859
|
const next = () => run(index + 1);
|
|
610
860
|
return instance.handle(request, next, ...parameters);
|
|
611
861
|
};
|
|
612
|
-
|
|
613
|
-
|
|
862
|
+
show();
|
|
863
|
+
try {
|
|
864
|
+
const response = await run(0);
|
|
865
|
+
return ran ? ran.result : response;
|
|
866
|
+
} finally {
|
|
867
|
+
restore();
|
|
868
|
+
}
|
|
614
869
|
}
|
|
615
870
|
|
|
616
|
-
//
|
|
617
|
-
import {
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
if (typeof value !== "object" || value === null) return false;
|
|
623
|
-
const candidate = value;
|
|
624
|
-
return typeof candidate.getKey === "function" && typeof candidate.getKeyName === "function" && typeof candidate.getTable === "function" && typeof candidate.exists === "boolean";
|
|
871
|
+
// WithPagination.ts
|
|
872
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
873
|
+
var PAGINATED = /* @__PURE__ */ Symbol.for("oriel:live.pagination");
|
|
874
|
+
function toPage(value) {
|
|
875
|
+
const page = typeof value === "number" ? value : typeof value === "string" && value.trim() !== "" ? Number(value) : Number.NaN;
|
|
876
|
+
return Number.isFinite(page) && page >= 1 ? Math.min(Math.floor(page), Number.MAX_SAFE_INTEGER) : 1;
|
|
625
877
|
}
|
|
626
|
-
|
|
627
|
-
const
|
|
628
|
-
|
|
629
|
-
};
|
|
630
|
-
var isTuple = (value) => Array.isArray(value) && value.length === 2 && typeof value[1] === "object" && value[1] !== null && !Array.isArray(value[1]) && typeof value[1].s === "string";
|
|
631
|
-
function classNameOf(registry, cls) {
|
|
632
|
-
return registry.nameOf(cls) ?? registry.register(cls);
|
|
878
|
+
async function hook(instance, name, ...args) {
|
|
879
|
+
const method = instance[name];
|
|
880
|
+
if (typeof method === "function") await method.apply(instance, args);
|
|
633
881
|
}
|
|
634
|
-
function
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
case "boolean":
|
|
640
|
-
return value;
|
|
641
|
-
case "number":
|
|
642
|
-
if (!Number.isFinite(value)) throw new LiveError(`Property [${path}] is ${value}: Live state must be JSON (finite numbers only).`);
|
|
643
|
-
return value;
|
|
644
|
-
case "bigint":
|
|
645
|
-
case "symbol":
|
|
646
|
-
case "function":
|
|
647
|
-
throw new LiveError(`Property [${path}] holds a ${typeof value}: Live state may only hold primitives, arrays, plain objects, Dates, models and collections.`);
|
|
648
|
-
}
|
|
649
|
-
if (value === null) return null;
|
|
650
|
-
const object = value;
|
|
651
|
-
if (seen.has(object)) throw new LiveError(`Property [${path}] contains a circular reference.`);
|
|
652
|
-
seen.add(object);
|
|
653
|
-
try {
|
|
654
|
-
if (object instanceof Date) return [object.toISOString(), { s: "dt" }];
|
|
655
|
-
if (Array.isArray(object)) return [object.map((item, i) => dehydrate(item, registry, `${path}.${i}`, seen)), { s: "arr" }];
|
|
656
|
-
if (isModel(object)) {
|
|
657
|
-
const name = classNameOf(registry, object.constructor);
|
|
658
|
-
if (!object.exists) throw new LiveError(`Property [${path}] holds an unsaved [${name}] model: only saved models can be kept between requests.`);
|
|
659
|
-
return [null, { s: "mdl", class: name, key: object.getKey() }];
|
|
882
|
+
function WithPagination(Base) {
|
|
883
|
+
class WithPaginationMixin extends Base {
|
|
884
|
+
page = 1;
|
|
885
|
+
nextPage() {
|
|
886
|
+
return this.setPage(toPage(this.page) + 1);
|
|
660
887
|
}
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
const first = items[0];
|
|
664
|
-
if (items.length > 0 && isModel(first) && items.every((item) => isModel(item) && item.constructor === first.constructor)) {
|
|
665
|
-
for (const [i, item] of items.entries()) {
|
|
666
|
-
if (!item.exists) throw new LiveError(`Property [${path}.${i}] holds an unsaved model.`);
|
|
667
|
-
}
|
|
668
|
-
const name = classNameOf(registry, first.constructor);
|
|
669
|
-
return [null, { s: "elcln", class: name, keys: items.map((item) => item.getKey()) }];
|
|
670
|
-
}
|
|
671
|
-
return [items.map((item, i) => dehydrate(item, registry, `${path}.${i}`, seen)), { s: "clctn" }];
|
|
888
|
+
previousPage() {
|
|
889
|
+
return this.setPage(Math.max(toPage(this.page) - 1, 1));
|
|
672
890
|
}
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
for (const [key, item] of Object.entries(object)) out[key] = dehydrate(item, registry, `${path}.${key}`, seen);
|
|
676
|
-
return [out, { s: "obj" }];
|
|
891
|
+
gotoPage(page) {
|
|
892
|
+
return this.setPage(page);
|
|
677
893
|
}
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
`Property [${path}] holds a [${type}] instance: Live state may only hold primitives, arrays, plain objects, Dates, models and collections.`
|
|
681
|
-
);
|
|
682
|
-
} finally {
|
|
683
|
-
seen.delete(object);
|
|
684
|
-
}
|
|
685
|
-
}
|
|
686
|
-
function modelClass(registry, name) {
|
|
687
|
-
if (typeof name !== "string" || !registry.has(name)) throw new LiveError(`Unable to hydrate the [${String(name)}] model: it is not registered.`);
|
|
688
|
-
return registry.resolve(name);
|
|
689
|
-
}
|
|
690
|
-
async function hydrate(value, registry) {
|
|
691
|
-
if (!isTuple(value)) return value;
|
|
692
|
-
const [inner, meta] = value;
|
|
693
|
-
switch (meta.s) {
|
|
694
|
-
case "u":
|
|
695
|
-
return void 0;
|
|
696
|
-
case "dt":
|
|
697
|
-
return new Date(String(inner));
|
|
698
|
-
case "arr":
|
|
699
|
-
return Promise.all(inner.map((item) => hydrate(item, registry)));
|
|
700
|
-
case "clctn":
|
|
701
|
-
return new Collection(await Promise.all(inner.map((item) => hydrate(item, registry))));
|
|
702
|
-
case "obj": {
|
|
703
|
-
const out = {};
|
|
704
|
-
for (const [key, item] of Object.entries(inner)) {
|
|
705
|
-
Object.defineProperty(out, key, { value: await hydrate(item, registry), enumerable: true, writable: true, configurable: true });
|
|
706
|
-
}
|
|
707
|
-
return out;
|
|
894
|
+
resetPage() {
|
|
895
|
+
return this.setPage(1);
|
|
708
896
|
}
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
const keyName = new cls().getKeyName();
|
|
715
|
-
const found = [...await cls.query().whereIn(keyName, keys).get()];
|
|
716
|
-
const byKey = new Map(found.map((model) => [String(model.getKey()), model]));
|
|
717
|
-
return new Collection(keys.map((key) => byKey.get(String(key))).filter((model) => model !== void 0));
|
|
897
|
+
async setPage(page) {
|
|
898
|
+
const next = toPage(page);
|
|
899
|
+
await hook(this, "updatingPage", next);
|
|
900
|
+
this.page = next;
|
|
901
|
+
await hook(this, "updatedPage", next);
|
|
718
902
|
}
|
|
719
903
|
}
|
|
720
|
-
|
|
904
|
+
Object.defineProperty(WithPaginationMixin.prototype, PAGINATED, { value: true });
|
|
905
|
+
return WithPaginationMixin;
|
|
721
906
|
}
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
var HOOKS = /* @__PURE__ */ new Set(["constructor", "render", "mount", "boot", "booted", "hydrate", "dehydrate", "updating", "updated", "rendering", "rendered", "exception"]);
|
|
725
|
-
var HOOK_PREFIX = /^(updating|updated|hydrate|dehydrate)[A-Z_]/;
|
|
726
|
-
var UNSAFE_SEGMENTS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
727
|
-
var BASE_API = new Set(Object.getOwnPropertyNames(Component.prototype));
|
|
728
|
-
var INDEX = /^(?:0|[1-9]\d*)$/;
|
|
729
|
-
function isArraySlot(array, segment) {
|
|
730
|
-
return INDEX.test(segment) && Number(segment) <= array.length;
|
|
907
|
+
function isPaginated(component) {
|
|
908
|
+
return component[PAGINATED] === true;
|
|
731
909
|
}
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
if (lockedOf(instance.constructor).has(method)) return false;
|
|
736
|
-
const isBase = (proto) => proto === Component.prototype || proto === Object.prototype;
|
|
737
|
-
for (let proto = Object.getPrototypeOf(instance); proto !== null && !isBase(proto); proto = Object.getPrototypeOf(proto)) {
|
|
738
|
-
const descriptor = Object.getOwnPropertyDescriptor(proto, method);
|
|
739
|
-
if (descriptor !== void 0) return typeof descriptor.value === "function";
|
|
740
|
-
}
|
|
741
|
-
return false;
|
|
910
|
+
var rendering = new AsyncLocalStorage();
|
|
911
|
+
function renderPaginated(component, url, run) {
|
|
912
|
+
return rendering.run(isPaginated(component) ? { component, url } : void 0, run);
|
|
742
913
|
}
|
|
743
|
-
function
|
|
744
|
-
|
|
745
|
-
const root = segments[0];
|
|
746
|
-
if (root === "" || root.startsWith("_") || segments.some((segment) => segment === "" || UNSAFE_SEGMENTS.has(segment))) return false;
|
|
747
|
-
if (!Object.hasOwn(instance, root)) return false;
|
|
748
|
-
const descriptor = Object.getOwnPropertyDescriptor(instance, root);
|
|
749
|
-
if (!descriptor.enumerable || descriptor.get !== void 0) return false;
|
|
750
|
-
const cls = instance.constructor;
|
|
751
|
-
if (lockedOf(cls).has(root) || computedOf(cls).has(root)) return false;
|
|
752
|
-
let node = descriptor.value;
|
|
753
|
-
for (const segment of segments.slice(1)) {
|
|
754
|
-
if (isModel(node) || node instanceof Collection2) return false;
|
|
755
|
-
if (Array.isArray(node) && !isArraySlot(node, segment)) return false;
|
|
756
|
-
if (typeof node !== "object" || node === null || !Object.hasOwn(node, segment)) return true;
|
|
757
|
-
node = node[segment];
|
|
758
|
-
}
|
|
759
|
-
return !isModel(node) && !(node instanceof Collection2);
|
|
914
|
+
function outsidePagination(run) {
|
|
915
|
+
return rendering.run(void 0, run);
|
|
760
916
|
}
|
|
917
|
+
var livePaginationState = {
|
|
918
|
+
currentPage(pageName) {
|
|
919
|
+
const current = rendering.getStore();
|
|
920
|
+
if (current === void 0 || pageName !== "page") return void 0;
|
|
921
|
+
const page = toPage(current.component.page);
|
|
922
|
+
current.component.page = page;
|
|
923
|
+
return page;
|
|
924
|
+
},
|
|
925
|
+
currentPath() {
|
|
926
|
+
return rendering.getStore()?.url;
|
|
927
|
+
}
|
|
928
|
+
};
|
|
761
929
|
|
|
762
930
|
// LiveComponentManager.ts
|
|
763
|
-
var renderContext = new
|
|
764
|
-
var pageRequest = new
|
|
931
|
+
var renderContext = new AsyncLocalStorage2();
|
|
932
|
+
var pageRequest = new AsyncLocalStorage2();
|
|
765
933
|
function optional(container, token2) {
|
|
766
934
|
if (!container.bound(token2)) return void 0;
|
|
767
935
|
try {
|
|
@@ -774,7 +942,7 @@ var isRecord = (value) => typeof value === "object" && value !== null && !Array.
|
|
|
774
942
|
function setOwn(object, key, value) {
|
|
775
943
|
Object.defineProperty(object, key, { value, enumerable: true, writable: true, configurable: true });
|
|
776
944
|
}
|
|
777
|
-
async function
|
|
945
|
+
async function hook2(instance, name, ...args) {
|
|
778
946
|
const method = instance[name];
|
|
779
947
|
if (typeof method === "function") await method.apply(instance, args);
|
|
780
948
|
}
|
|
@@ -825,7 +993,7 @@ var LiveComponentManager = class {
|
|
|
825
993
|
setOwn(context.children, childKey, existing);
|
|
826
994
|
return `<${existing[0]} o:id="${escapeAttribute(existing[1])}"></${existing[0]}>`;
|
|
827
995
|
}
|
|
828
|
-
const result = await this.mountComponent(component, params);
|
|
996
|
+
const result = await outsidePagination(() => this.mountComponent(component, params));
|
|
829
997
|
setOwn(context.children, childKey, [result.tag, result.snapshot.memo.id]);
|
|
830
998
|
return result.html;
|
|
831
999
|
}
|
|
@@ -837,14 +1005,14 @@ var LiveComponentManager = class {
|
|
|
837
1005
|
const state = stateOf(instance);
|
|
838
1006
|
state.id = randomBytes(15).toString("base64url");
|
|
839
1007
|
state.name = name;
|
|
840
|
-
await
|
|
1008
|
+
await hook2(instance, "boot");
|
|
841
1009
|
for (const [property, value] of Object.entries(params)) {
|
|
842
1010
|
if (Object.hasOwn(instance, property)) instance[property] = value;
|
|
843
1011
|
}
|
|
844
1012
|
this.prepareComputed(instance);
|
|
845
|
-
await hook(instance, "mount", params);
|
|
846
1013
|
const request = optional(this.container(), HttpRequest);
|
|
847
1014
|
const page = pageRequest.getStore() ?? { path: request?.path() ?? "", method: request?.method() ?? "GET" };
|
|
1015
|
+
await renderPaginated(instance, this.pageUrl(page.path), () => hook2(instance, "mount", params));
|
|
848
1016
|
const memo = { id: state.id, name, path: page.path, method: page.method, children: {}, errors: {} };
|
|
849
1017
|
const { html, tag } = await this.render(instance, memo, {});
|
|
850
1018
|
const effects = this.effects(instance);
|
|
@@ -872,22 +1040,24 @@ var LiveComponentManager = class {
|
|
|
872
1040
|
state.id = snapshot.memo.id;
|
|
873
1041
|
state.name = snapshot.memo.name;
|
|
874
1042
|
state.errors = { ...snapshot.memo.errors };
|
|
875
|
-
await
|
|
1043
|
+
await hook2(instance, "boot");
|
|
876
1044
|
const registry = this.registry();
|
|
877
1045
|
for (const [property, value] of Object.entries(snapshot.data)) {
|
|
878
1046
|
Object.defineProperty(instance, property, { value: await hydrate(value, registry), enumerable: true, writable: true, configurable: true });
|
|
879
1047
|
}
|
|
880
1048
|
this.prepareComputed(instance);
|
|
881
|
-
await hook(instance, "hydrate");
|
|
882
1049
|
const returns = [];
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
1050
|
+
await renderPaginated(instance, this.pageUrl(snapshot.memo.path), async () => {
|
|
1051
|
+
await hook2(instance, "hydrate");
|
|
1052
|
+
try {
|
|
1053
|
+
for (const [path, value] of Object.entries(updates)) await this.applyUpdate(instance, path, value);
|
|
1054
|
+
for (const call of calls) returns.push(await this.call(instance, call.method, call.params));
|
|
1055
|
+
} catch (error) {
|
|
1056
|
+
const errors = error.errors;
|
|
1057
|
+
if (error.kind !== "Validation" || !isRecord(errors)) throw error;
|
|
1058
|
+
state.errors = { ...errors };
|
|
1059
|
+
}
|
|
1060
|
+
});
|
|
891
1061
|
const memo = { ...snapshot.memo, children: { ...snapshot.memo.children } };
|
|
892
1062
|
const effects = this.effects(instance);
|
|
893
1063
|
let html;
|
|
@@ -902,31 +1072,36 @@ var LiveComponentManager = class {
|
|
|
902
1072
|
return { instance, snapshot: await this.snapshot(instance, memo), effects, html, tag };
|
|
903
1073
|
}
|
|
904
1074
|
/**
|
|
905
|
-
* The route action of POST /oriel/live/update. Each component's update runs behind the
|
|
906
|
-
* that rendered its page (Livewire's
|
|
907
|
-
*
|
|
1075
|
+
* The route action of POST /oriel/live/update. Each component's update runs behind the persistent middleware of the
|
|
1076
|
+
* route that rendered its page (Livewire's): `auth`, `verified`, `guest` and `can`, with the page route's parameters
|
|
1077
|
+
* bound again. A signed-out tab gets a 401, a revoked ability a 403 and a deleted model a 404, before the component
|
|
1078
|
+
* hydrates. When a middleware answers instead (a redirect), that is the response. A key first flashed during the
|
|
1079
|
+
* update shows in this response only, unless the update redirects.
|
|
908
1080
|
*/
|
|
909
1081
|
async handle(request) {
|
|
910
1082
|
if (!this.isLiveRequest(request)) throw new InvalidLiveRequestError("Live update requests need the X-Oriel-Live header.");
|
|
911
1083
|
const container = this.container();
|
|
912
1084
|
const persistent = optional(container, ConfigRepository)?.get("live.persistent_middleware", PERSISTENT_MIDDLEWARE) ?? PERSISTENT_MIDDLEWARE;
|
|
913
1085
|
const components = [];
|
|
1086
|
+
const flashed = trackFlash(optional(container, Session2));
|
|
914
1087
|
for (const component of this.payload(request).components) {
|
|
915
1088
|
const { memo } = this.verify(component.snapshot);
|
|
916
|
-
const
|
|
1089
|
+
const pass = persistentPass(container.make(Router), persistent, memo.method, memo.path);
|
|
1090
|
+
const update = () => this.updateComponent(component.snapshot, component.updates, component.calls);
|
|
917
1091
|
const result = await pageRequest.run(
|
|
918
1092
|
{ path: memo.path, method: memo.method },
|
|
919
|
-
() =>
|
|
1093
|
+
() => pass === void 0 || pass.middleware.length === 0 ? update() : throughMiddleware(container, request, pass, update)
|
|
920
1094
|
);
|
|
921
1095
|
if (result instanceof Response) return result;
|
|
922
1096
|
components.push({ snapshot: JSON.stringify(result.snapshot), effects: result.effects });
|
|
923
1097
|
}
|
|
1098
|
+
flashed(components.some((component) => component.effects.redirect !== void 0));
|
|
924
1099
|
return { components };
|
|
925
1100
|
}
|
|
926
1101
|
/** A full-page component (Route.get('/counter', Counter)): mounted with the route parameters, inside the layout. */
|
|
927
1102
|
async page(component) {
|
|
928
1103
|
const container = this.container();
|
|
929
|
-
const params = { ...optional(container,
|
|
1104
|
+
const params = { ...optional(container, CurrentRoute2)?.parameters ?? {} };
|
|
930
1105
|
const cls = component.constructor;
|
|
931
1106
|
const result = await this.mountComponent(cls, params, component);
|
|
932
1107
|
const redirect = result.effects.redirect;
|
|
@@ -995,8 +1170,8 @@ var LiveComponentManager = class {
|
|
|
995
1170
|
const [root, ...rest] = path.split(".");
|
|
996
1171
|
const key = rest.length > 0 ? rest.join(".") : void 0;
|
|
997
1172
|
const studly = Str2.studly(root);
|
|
998
|
-
await
|
|
999
|
-
await
|
|
1173
|
+
await hook2(instance, "updating", path, value);
|
|
1174
|
+
await hook2(instance, `updating${studly}`, value, key);
|
|
1000
1175
|
const target = instance;
|
|
1001
1176
|
if (rest.length === 0) target[root] = value;
|
|
1002
1177
|
else {
|
|
@@ -1014,8 +1189,8 @@ var LiveComponentManager = class {
|
|
|
1014
1189
|
assertSlot(node, last);
|
|
1015
1190
|
Object.defineProperty(node, last, { value, enumerable: true, writable: true, configurable: true });
|
|
1016
1191
|
}
|
|
1017
|
-
await
|
|
1018
|
-
await
|
|
1192
|
+
await hook2(instance, "updated", path, value);
|
|
1193
|
+
await hook2(instance, `updated${studly}`, value, key);
|
|
1019
1194
|
}
|
|
1020
1195
|
/** Runs one call from the browser: a magic action, an event, or a public method. Returns its return value. */
|
|
1021
1196
|
async call(instance, method, params) {
|
|
@@ -1043,18 +1218,35 @@ var LiveComponentManager = class {
|
|
|
1043
1218
|
if (!isCallable(instance, method)) throw new MethodNotCallableError(method, name);
|
|
1044
1219
|
return instance[method](...params);
|
|
1045
1220
|
}
|
|
1046
|
-
/**
|
|
1221
|
+
/**
|
|
1222
|
+
* Renders the component (rendering/rendered hooks, nested children tracked) and checks for a single root. A
|
|
1223
|
+
* WithPagination component's page and page URL are what paginate() reads while it renders, as in its mount() and in
|
|
1224
|
+
* its update's hooks and actions.
|
|
1225
|
+
*/
|
|
1047
1226
|
async render(instance, memo, previous) {
|
|
1048
|
-
await
|
|
1227
|
+
await hook2(instance, "rendering");
|
|
1049
1228
|
const context = { previous, children: {}, index: 0 };
|
|
1050
|
-
const html = await renderContext.run(
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1229
|
+
const html = await renderContext.run(
|
|
1230
|
+
context,
|
|
1231
|
+
() => renderPaginated(instance, this.pageUrl(memo.path), async () => {
|
|
1232
|
+
const output = await instance.render();
|
|
1233
|
+
return typeof output === "string" ? output : this.container().make(ViewFactory).renderToString(output);
|
|
1234
|
+
})
|
|
1235
|
+
);
|
|
1236
|
+
await hook2(instance, "rendered", html);
|
|
1055
1237
|
memo.children = context.children;
|
|
1056
1238
|
return { html: html.trim(), tag: rootTag(html, memo.name) };
|
|
1057
1239
|
}
|
|
1240
|
+
/** The absolute URL of the page a memo path belongs to, on the current request's origin ('/' + path without one). */
|
|
1241
|
+
pageUrl(path) {
|
|
1242
|
+
const relative3 = `/${path.replace(/^\/+/, "")}`;
|
|
1243
|
+
const url = optional(this.container(), HttpRequest)?.url();
|
|
1244
|
+
try {
|
|
1245
|
+
return url === void 0 ? relative3 : new URL(relative3, url).href;
|
|
1246
|
+
} catch {
|
|
1247
|
+
return relative3;
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1058
1250
|
/** Dispatches, redirects: the effects every response carries. */
|
|
1059
1251
|
effects(instance) {
|
|
1060
1252
|
const state = stateOf(instance);
|
|
@@ -1073,7 +1265,7 @@ var LiveComponentManager = class {
|
|
|
1073
1265
|
}
|
|
1074
1266
|
/** dehydrate() hook, then the state as snapshot data, the error bag, and the checksum over both. */
|
|
1075
1267
|
async snapshot(instance, memo) {
|
|
1076
|
-
await
|
|
1268
|
+
await hook2(instance, "dehydrate");
|
|
1077
1269
|
const registry = this.registry();
|
|
1078
1270
|
const data = {};
|
|
1079
1271
|
for (const [property, value] of Object.entries(instance)) data[property] = dehydrate(value, registry, `${memo.name}.${property}`);
|
|
@@ -1083,7 +1275,7 @@ var LiveComponentManager = class {
|
|
|
1083
1275
|
};
|
|
1084
1276
|
|
|
1085
1277
|
// LiveServiceProvider.ts
|
|
1086
|
-
import { ConfigRepository as ConfigRepository3, ConsoleKernel, HttpRequest as HttpRequest2, LiveManager as LiveManager4, Router as Router2 } from "@orieljs/contracts";
|
|
1278
|
+
import { ConfigRepository as ConfigRepository3, ConsoleKernel, HttpRequest as HttpRequest2, LiveManager as LiveManager4, PaginationState, Router as Router2 } from "@orieljs/contracts";
|
|
1087
1279
|
import { inject as inject4 } from "@orieljs/container";
|
|
1088
1280
|
import { ServiceProvider } from "@orieljs/support";
|
|
1089
1281
|
|
|
@@ -1160,6 +1352,7 @@ var LiveServiceProvider = class extends ServiceProvider {
|
|
|
1160
1352
|
this.mergeConfigFrom({ class_path: this.app.appPath("Live"), layout: "components.layouts.app", persistent_middleware: [...PERSISTENT_MIDDLEWARE] }, "live");
|
|
1161
1353
|
this.app.singleton(LiveComponentManager, () => new LiveComponentManager(this.app));
|
|
1162
1354
|
this.app.singleton(LiveManager4, (container) => container.make(LiveComponentManager));
|
|
1355
|
+
this.app.singleton(PaginationState, () => livePaginationState);
|
|
1163
1356
|
}
|
|
1164
1357
|
async boot() {
|
|
1165
1358
|
const manager = this.app.make(LiveComponentManager);
|
|
@@ -1173,6 +1366,42 @@ var LiveServiceProvider = class extends ServiceProvider {
|
|
|
1173
1366
|
}
|
|
1174
1367
|
}
|
|
1175
1368
|
};
|
|
1369
|
+
|
|
1370
|
+
// Pagination.ts
|
|
1371
|
+
function urlWindow(current, last, onEachSide) {
|
|
1372
|
+
const range = (from, to) => Array.from({ length: Math.max(to - from + 1, 0) }, (_, index) => from + index);
|
|
1373
|
+
if (last < onEachSide * 2 + 8) return range(1, last);
|
|
1374
|
+
const size = onEachSide + 4;
|
|
1375
|
+
if (current <= size) return [...range(1, size + onEachSide), "...", ...range(last - 1, last)];
|
|
1376
|
+
if (current > last - size) return [...range(1, 2), "...", ...range(last - (size + (onEachSide - 1)), last)];
|
|
1377
|
+
return [...range(1, 2), "...", ...range(current - onEachSide, current + onEachSide), "...", ...range(last - 1, last)];
|
|
1378
|
+
}
|
|
1379
|
+
function button(label, action, attributes = "") {
|
|
1380
|
+
const live2 = action === void 0 ? " disabled" : ` o:click="${escapeAttribute(action)}"`;
|
|
1381
|
+
return `<button type="button" class="btn btn-secondary"${attributes}${live2}>${escapeAttribute(label)}</button>`;
|
|
1382
|
+
}
|
|
1383
|
+
function Pagination({ paginator, class: className }) {
|
|
1384
|
+
if (typeof paginator.nextCursor === "function") {
|
|
1385
|
+
throw new LiveError("<Pagination> needs paginate() or simplePaginate(): a cursor paginator has no page numbers.");
|
|
1386
|
+
}
|
|
1387
|
+
const current = paginator.currentPage();
|
|
1388
|
+
const onFirst = paginator.onFirstPage();
|
|
1389
|
+
const hasMore = paginator.hasMorePages();
|
|
1390
|
+
if (onFirst && !hasMore) return { toHtml: () => "" };
|
|
1391
|
+
const parts = [button("\xAB Previous", onFirst ? void 0 : "previousPage")];
|
|
1392
|
+
if (typeof paginator.lastPage === "function") {
|
|
1393
|
+
const onEachSide = Number.isInteger(paginator.onEachSideCount) && paginator.onEachSideCount >= 0 ? paginator.onEachSideCount : 3;
|
|
1394
|
+
for (const page of urlWindow(current, paginator.lastPage(), onEachSide)) {
|
|
1395
|
+
if (page === "...") parts.push('<span aria-hidden="true">...</span>');
|
|
1396
|
+
else if (page === current) parts.push(`<button type="button" class="btn" aria-current="page" disabled>${page}</button>`);
|
|
1397
|
+
else parts.push(button(String(page), `gotoPage(${page})`, ` aria-label="Go to page ${page}"`));
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
parts.push(button("Next \xBB", hasMore ? "nextPage" : void 0));
|
|
1401
|
+
const classes = className === void 0 || className === "" ? "" : ` class="${escapeAttribute(className)}"`;
|
|
1402
|
+
const html = `<nav aria-label="Pagination"${classes}>${parts.join("")}</nav>`;
|
|
1403
|
+
return { toHtml: () => html };
|
|
1404
|
+
}
|
|
1176
1405
|
export {
|
|
1177
1406
|
Component,
|
|
1178
1407
|
Computed,
|
|
@@ -1188,11 +1417,13 @@ export {
|
|
|
1188
1417
|
MethodNotCallableError,
|
|
1189
1418
|
On,
|
|
1190
1419
|
PERSISTENT_MIDDLEWARE,
|
|
1420
|
+
Pagination,
|
|
1191
1421
|
PendingEvent,
|
|
1192
1422
|
PropertyNotUpdatableError,
|
|
1193
1423
|
TestRequestScope,
|
|
1194
1424
|
Testable,
|
|
1195
1425
|
UPDATE_URI,
|
|
1426
|
+
WithPagination,
|
|
1196
1427
|
componentName,
|
|
1197
1428
|
live,
|
|
1198
1429
|
persistentMiddleware
|