@carno.js/live 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/LiveEngine.d.ts +89 -0
- package/dist/LiveEngine.js +520 -0
- package/dist/LivePlugin.d.ts +55 -0
- package/dist/LivePlugin.js +165 -0
- package/dist/LiveService.d.ts +32 -0
- package/dist/LiveService.js +51 -0
- package/dist/auth/authorizer.d.ts +33 -0
- package/dist/auth/authorizer.js +32 -0
- package/dist/bus/InProcessBus.d.ts +8 -0
- package/dist/bus/InProcessBus.js +29 -0
- package/dist/bus/InvalidationBus.d.ts +15 -0
- package/dist/bus/InvalidationBus.js +2 -0
- package/dist/bus/PgNotifyBus.d.ts +55 -0
- package/dist/bus/PgNotifyBus.js +138 -0
- package/dist/client/angular.d.ts +19 -0
- package/dist/client/angular.js +71 -0
- package/dist/client/core.d.ts +111 -0
- package/dist/client/core.js +337 -0
- package/dist/client/hydrate.d.ts +19 -0
- package/dist/client/hydrate.js +65 -0
- package/dist/client/optimistic.d.ts +17 -0
- package/dist/client/optimistic.js +2 -0
- package/dist/client/react.d.ts +22 -0
- package/dist/client/react.js +69 -0
- package/dist/client/transport.d.ts +133 -0
- package/dist/client/transport.js +431 -0
- package/dist/client/vanilla.d.ts +54 -0
- package/dist/client/vanilla.js +111 -0
- package/dist/client/vue.d.ts +12 -0
- package/dist/client/vue.js +35 -0
- package/dist/config.d.ts +32 -0
- package/dist/config.js +21 -0
- package/dist/decorators/Live.d.ts +4 -0
- package/dist/decorators/Live.js +17 -0
- package/dist/emitters/AppEmitter.d.ts +20 -0
- package/dist/emitters/AppEmitter.js +59 -0
- package/dist/emitters/pg-listener.d.ts +50 -0
- package/dist/emitters/pg-listener.js +120 -0
- package/dist/emitters/pg-notify-emitter.d.ts +59 -0
- package/dist/emitters/pg-notify-emitter.js +105 -0
- package/dist/emitters/pg-trigger-sql.d.ts +27 -0
- package/dist/emitters/pg-trigger-sql.js +107 -0
- package/dist/emitters/statement-keys.d.ts +16 -0
- package/dist/emitters/statement-keys.js +182 -0
- package/dist/graph/DependencyGraph.d.ts +27 -0
- package/dist/graph/DependencyGraph.js +118 -0
- package/dist/graph/SubscriptionRegistry.d.ts +22 -0
- package/dist/graph/SubscriptionRegistry.js +92 -0
- package/dist/graph/dep-key.d.ts +14 -0
- package/dist/graph/dep-key.js +37 -0
- package/dist/graph/types.d.ts +13 -0
- package/dist/graph/types.js +2 -0
- package/dist/http/etag.d.ts +41 -0
- package/dist/http/etag.js +109 -0
- package/dist/index.d.ts +56 -0
- package/dist/index.js +111 -0
- package/dist/metadata.d.ts +16 -0
- package/dist/metadata.js +4 -0
- package/dist/observability.d.ts +25 -0
- package/dist/observability.js +44 -0
- package/dist/patch/PatchEngine.d.ts +20 -0
- package/dist/patch/PatchEngine.js +180 -0
- package/dist/patch/types.d.ts +33 -0
- package/dist/patch/types.js +2 -0
- package/dist/resource/ResourceRegistry.d.ts +32 -0
- package/dist/resource/ResourceRegistry.js +126 -0
- package/dist/resource/dependency-context.d.ts +25 -0
- package/dist/resource/dependency-context.js +45 -0
- package/dist/resource/instance-id.d.ts +17 -0
- package/dist/resource/instance-id.js +61 -0
- package/dist/resource/prefetch.d.ts +20 -0
- package/dist/resource/prefetch.js +31 -0
- package/dist/resource/route-executor.d.ts +13 -0
- package/dist/resource/route-executor.js +86 -0
- package/dist/resource/types.d.ts +26 -0
- package/dist/resource/types.js +2 -0
- package/dist/runtime.d.ts +41 -0
- package/dist/runtime.js +47 -0
- package/dist/shared/canonical.d.ts +14 -0
- package/dist/shared/canonical.js +60 -0
- package/dist/shared/descriptor.d.ts +44 -0
- package/dist/shared/descriptor.js +19 -0
- package/dist/shared/hash.d.ts +12 -0
- package/dist/shared/hash.js +29 -0
- package/dist/shared/inputs.d.ts +19 -0
- package/dist/shared/inputs.js +2 -0
- package/dist/shared/protocol.d.ts +83 -0
- package/dist/shared/protocol.js +9 -0
- package/dist/transport/FanTransport.d.ts +18 -0
- package/dist/transport/FanTransport.js +29 -0
- package/dist/transport/LiveGateway.d.ts +16 -0
- package/dist/transport/LiveGateway.js +126 -0
- package/dist/transport/SocketTransport.d.ts +18 -0
- package/dist/transport/SocketTransport.js +40 -0
- package/dist/transport/SseTransport.d.ts +30 -0
- package/dist/transport/SseTransport.js +92 -0
- package/dist/transport/scope-resolver.d.ts +20 -0
- package/dist/transport/scope-resolver.js +17 -0
- package/dist/transport/sse-routes.d.ts +24 -0
- package/dist/transport/sse-routes.js +78 -0
- package/package.json +92 -0
- package/src/LiveEngine.ts +730 -0
- package/src/LivePlugin.ts +253 -0
- package/src/LiveService.ts +47 -0
- package/src/auth/authorizer.ts +56 -0
- package/src/bus/InProcessBus.ts +28 -0
- package/src/bus/InvalidationBus.ts +17 -0
- package/src/bus/PgNotifyBus.ts +188 -0
- package/src/client/angular.ts +115 -0
- package/src/client/core.ts +492 -0
- package/src/client/hydrate.ts +80 -0
- package/src/client/optimistic.ts +19 -0
- package/src/client/react.ts +113 -0
- package/src/client/transport.ts +568 -0
- package/src/client/vanilla.ts +165 -0
- package/src/client/vue.ts +67 -0
- package/src/config.ts +49 -0
- package/src/decorators/Live.ts +16 -0
- package/src/emitters/AppEmitter.ts +69 -0
- package/src/emitters/pg-listener.ts +171 -0
- package/src/emitters/pg-notify-emitter.ts +143 -0
- package/src/emitters/pg-trigger-sql.ts +111 -0
- package/src/emitters/statement-keys.ts +229 -0
- package/src/graph/DependencyGraph.ts +147 -0
- package/src/graph/SubscriptionRegistry.ts +109 -0
- package/src/graph/dep-key.ts +41 -0
- package/src/graph/types.ts +15 -0
- package/src/http/etag.ts +144 -0
- package/src/index.ts +81 -0
- package/src/metadata.ts +21 -0
- package/src/observability.ts +54 -0
- package/src/patch/PatchEngine.ts +235 -0
- package/src/patch/types.ts +39 -0
- package/src/resource/ResourceRegistry.ts +178 -0
- package/src/resource/dependency-context.ts +48 -0
- package/src/resource/instance-id.ts +65 -0
- package/src/resource/prefetch.ts +49 -0
- package/src/resource/route-executor.ts +134 -0
- package/src/resource/types.ts +35 -0
- package/src/runtime.ts +69 -0
- package/src/shared/canonical.ts +63 -0
- package/src/shared/descriptor.ts +44 -0
- package/src/shared/hash.ts +31 -0
- package/src/shared/inputs.ts +20 -0
- package/src/shared/protocol.ts +96 -0
- package/src/transport/FanTransport.ts +34 -0
- package/src/transport/LiveGateway.ts +124 -0
- package/src/transport/SocketTransport.ts +43 -0
- package/src/transport/SseTransport.ts +112 -0
- package/src/transport/scope-resolver.ts +25 -0
- package/src/transport/sse-routes.ts +94 -0
- package/test/acceptance-fase-2.test.ts +248 -0
- package/test/acceptance-fase-3.test.ts +216 -0
- package/test/acceptance.test.ts +306 -0
- package/test/angular-adapter.test.ts +151 -0
- package/test/app-emitter.test.ts +112 -0
- package/test/authorization.test.ts +175 -0
- package/test/client-core.test.ts +287 -0
- package/test/client-transport.test.ts +154 -0
- package/test/dependency-graph.test.ts +83 -0
- package/test/etag.test.ts +193 -0
- package/test/fan-transport.test.ts +62 -0
- package/test/happydom.ts +24 -0
- package/test/inputs-body.test.ts +81 -0
- package/test/instance-id.test.ts +104 -0
- package/test/live-engine.test.ts +593 -0
- package/test/live-post.test.ts +81 -0
- package/test/metrics.test.ts +131 -0
- package/test/optimistic.test.ts +166 -0
- package/test/orm-integration.test.ts +75 -0
- package/test/patch-engine.test.ts +121 -0
- package/test/pg-bus.test.ts +162 -0
- package/test/pg-listen-probe.test.ts +56 -0
- package/test/pg-listener.test.ts +134 -0
- package/test/pg-notify-emitter.test.ts +113 -0
- package/test/pg-notify-integration.test.ts +88 -0
- package/test/polling-auth.test.ts +169 -0
- package/test/prefetch.test.ts +137 -0
- package/test/react-adapter.test.tsx +83 -0
- package/test/react-rerender.test.tsx +131 -0
- package/test/resource-registry-helper.ts +35 -0
- package/test/resource-registry.test.ts +144 -0
- package/test/route-pipeline.test.ts +139 -0
- package/test/shared.test.ts +53 -0
- package/test/sse-routes.test.ts +204 -0
- package/test/sse-transport.test.ts +91 -0
- package/test/statement-keys.test.ts +126 -0
- package/test/subscription-registry.test.ts +61 -0
- package/test/transport-ladder.test.ts +293 -0
- package/test/transport.test.ts +274 -0
- package/test/types/optimistic-types.ts +34 -0
- package/test/use-live-action.test.ts +127 -0
- package/test/use-live.test.ts +109 -0
- package/test/vanilla-adapter.test.ts +200 -0
- package/test/vue-adapter.test.ts +122 -0
- package/tsconfig.json +15 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/tsconfig.types.json +10 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { CarnoClosure, CarnoMiddleware, Context } from '@carno.js/core';
|
|
2
|
+
import type { LiveInputs, LiveScope } from '../resource/types';
|
|
3
|
+
export interface LiveRoutePath {
|
|
4
|
+
method: string;
|
|
5
|
+
path: string;
|
|
6
|
+
resourceId?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface LivePollingRequest {
|
|
9
|
+
resourceId: string;
|
|
10
|
+
inputs: LiveInputs;
|
|
11
|
+
request: Request;
|
|
12
|
+
}
|
|
13
|
+
export type LivePollingGuard = (request: LivePollingRequest) => LiveScope | null | Promise<LiveScope | null>;
|
|
14
|
+
/** `/cards/:id` matches `/cards/42` and nothing deeper. */
|
|
15
|
+
export declare function pathMatcher(pattern: string): RegExp;
|
|
16
|
+
/**
|
|
17
|
+
* Content-hash `ETag` on live GET routes, and `304` when the client already
|
|
18
|
+
* holds that content.
|
|
19
|
+
*
|
|
20
|
+
* This is the bottom rung of §8.4: a client with neither WebSocket nor SSE
|
|
21
|
+
* polls the same route the SPA calls, and pays for a body only when something
|
|
22
|
+
* changed. The hash is the canonical one -- the same function the engine uses
|
|
23
|
+
* to decide whether a recompute produced a patch -- so reordered JSON keys are
|
|
24
|
+
* the same content, not a change.
|
|
25
|
+
*
|
|
26
|
+
* Scoped to live routes on purpose. Putting an `ETag` on every GET in the
|
|
27
|
+
* application would change the behaviour of routes that asked for none of this.
|
|
28
|
+
*/
|
|
29
|
+
export declare class LiveETagMiddleware implements CarnoMiddleware {
|
|
30
|
+
private readonly options;
|
|
31
|
+
private matchers;
|
|
32
|
+
private pollingGuard;
|
|
33
|
+
constructor(paths: LiveRoutePath[], options?: {
|
|
34
|
+
enabled?: boolean;
|
|
35
|
+
});
|
|
36
|
+
/** The plugin knows the live routes only after bootstrap. */
|
|
37
|
+
setPaths(paths: LiveRoutePath[]): void;
|
|
38
|
+
setPollingGuard(guard: LivePollingGuard): void;
|
|
39
|
+
handle(ctx: Context, next: CarnoClosure): Promise<Response | void>;
|
|
40
|
+
private routeFor;
|
|
41
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LiveETagMiddleware = void 0;
|
|
4
|
+
exports.pathMatcher = pathMatcher;
|
|
5
|
+
const canonical_1 = require("../shared/canonical");
|
|
6
|
+
const hash_1 = require("../shared/hash");
|
|
7
|
+
const protocol_1 = require("../shared/protocol");
|
|
8
|
+
/** `/cards/:id` matches `/cards/42` and nothing deeper. */
|
|
9
|
+
function pathMatcher(pattern) {
|
|
10
|
+
const source = pattern
|
|
11
|
+
.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
12
|
+
.replace(/:[A-Za-z_][A-Za-z0-9_]*/g, '[^/]+');
|
|
13
|
+
return new RegExp(`^${source}$`);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Content-hash `ETag` on live GET routes, and `304` when the client already
|
|
17
|
+
* holds that content.
|
|
18
|
+
*
|
|
19
|
+
* This is the bottom rung of §8.4: a client with neither WebSocket nor SSE
|
|
20
|
+
* polls the same route the SPA calls, and pays for a body only when something
|
|
21
|
+
* changed. The hash is the canonical one -- the same function the engine uses
|
|
22
|
+
* to decide whether a recompute produced a patch -- so reordered JSON keys are
|
|
23
|
+
* the same content, not a change.
|
|
24
|
+
*
|
|
25
|
+
* Scoped to live routes on purpose. Putting an `ETag` on every GET in the
|
|
26
|
+
* application would change the behaviour of routes that asked for none of this.
|
|
27
|
+
*/
|
|
28
|
+
class LiveETagMiddleware {
|
|
29
|
+
constructor(paths, options = {}) {
|
|
30
|
+
this.options = options;
|
|
31
|
+
this.matchers = [];
|
|
32
|
+
this.pollingGuard = null;
|
|
33
|
+
this.setPaths(paths);
|
|
34
|
+
}
|
|
35
|
+
/** The plugin knows the live routes only after bootstrap. */
|
|
36
|
+
setPaths(paths) {
|
|
37
|
+
this.matchers = paths
|
|
38
|
+
.filter(entry => entry.method.toUpperCase() === 'GET')
|
|
39
|
+
.map(entry => ({ matcher: pathMatcher(entry.path), resourceId: entry.resourceId }));
|
|
40
|
+
}
|
|
41
|
+
setPollingGuard(guard) {
|
|
42
|
+
this.pollingGuard = guard;
|
|
43
|
+
}
|
|
44
|
+
async handle(ctx, next) {
|
|
45
|
+
const polling = ctx.req.headers.get(protocol_1.LIVE_POLL_HEADER) === '1';
|
|
46
|
+
const requestedResource = polling
|
|
47
|
+
? ctx.req.headers.get(protocol_1.LIVE_RESOURCE_HEADER) ?? undefined
|
|
48
|
+
: undefined;
|
|
49
|
+
const route = ctx.method.toUpperCase() === 'GET'
|
|
50
|
+
? this.routeFor(ctx.path, requestedResource)
|
|
51
|
+
: undefined;
|
|
52
|
+
if (polling && (!requestedResource || !route)) {
|
|
53
|
+
return new Response('Forbidden', { status: 403 });
|
|
54
|
+
}
|
|
55
|
+
if (!route) {
|
|
56
|
+
return next();
|
|
57
|
+
}
|
|
58
|
+
if (ctx.req.headers.get(protocol_1.LIVE_POLL_HEADER) === '1') {
|
|
59
|
+
const resourceId = route.resourceId;
|
|
60
|
+
const scope = resourceId && this.pollingGuard
|
|
61
|
+
? await this.pollingGuard({
|
|
62
|
+
resourceId,
|
|
63
|
+
inputs: { params: ctx.params, query: ctx.query },
|
|
64
|
+
request: ctx.req
|
|
65
|
+
})
|
|
66
|
+
: null;
|
|
67
|
+
if (!scope) {
|
|
68
|
+
return new Response('Forbidden', { status: 403 });
|
|
69
|
+
}
|
|
70
|
+
if (scope.headers) {
|
|
71
|
+
const headers = new Headers(ctx.req.headers);
|
|
72
|
+
new Headers(scope.headers).forEach((value, key) => headers.set(key, value));
|
|
73
|
+
ctx.req = new Request(ctx.req, { headers });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (this.options.enabled === false) {
|
|
77
|
+
return next();
|
|
78
|
+
}
|
|
79
|
+
const response = await next();
|
|
80
|
+
if (response.status !== 200) {
|
|
81
|
+
return response;
|
|
82
|
+
}
|
|
83
|
+
const contentType = response.headers.get('Content-Type') ?? '';
|
|
84
|
+
if (!contentType.includes('application/json')) {
|
|
85
|
+
return response;
|
|
86
|
+
}
|
|
87
|
+
// Reading the body consumes the stream, so everything below hands the
|
|
88
|
+
// caller a rebuilt response rather than the one it just drained.
|
|
89
|
+
const body = await response.clone().text();
|
|
90
|
+
let tag;
|
|
91
|
+
try {
|
|
92
|
+
tag = `"${(0, hash_1.fnv1a64)((0, canonical_1.canonical)(JSON.parse(body)))}"`;
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
// Content-Type said JSON and it is not. Not our problem to fix.
|
|
96
|
+
return response;
|
|
97
|
+
}
|
|
98
|
+
if (ctx.req.headers.get('If-None-Match') === tag) {
|
|
99
|
+
return new Response(null, { status: 304, headers: { ETag: tag } });
|
|
100
|
+
}
|
|
101
|
+
const headers = new Headers(response.headers);
|
|
102
|
+
headers.set('ETag', tag);
|
|
103
|
+
return new Response(body, { status: 200, headers });
|
|
104
|
+
}
|
|
105
|
+
routeFor(path, resourceId) {
|
|
106
|
+
return this.matchers.find(route => route.matcher.test(path) && (resourceId === undefined || route.resourceId === resourceId));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
exports.LiveETagMiddleware = LiveETagMiddleware;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import 'reflect-metadata';
|
|
2
|
+
export { Live } from './decorators/Live';
|
|
3
|
+
export { LIVE_META } from './metadata';
|
|
4
|
+
export type { LiveMeta, LiveOptions, LiveShared } from './metadata';
|
|
5
|
+
export { LivePlugin } from './LivePlugin';
|
|
6
|
+
export type { LivePluginOptions } from './LivePlugin';
|
|
7
|
+
export { closeLiveRuntime } from './runtime';
|
|
8
|
+
export { LiveService } from './LiveService';
|
|
9
|
+
export { LiveEngine } from './LiveEngine';
|
|
10
|
+
export type { LiveTransport, LiveStats } from './LiveEngine';
|
|
11
|
+
export { LiveMetrics } from './observability';
|
|
12
|
+
export type { MetricSink } from './observability';
|
|
13
|
+
export { DEFAULT_LIVE_CONFIG, resolveLiveConfig } from './config';
|
|
14
|
+
export type { LiveConfig } from './config';
|
|
15
|
+
export { ConnectionScopeResolver } from './transport/scope-resolver';
|
|
16
|
+
export type { LiveHandshake, LiveScopeResolver } from './transport/scope-resolver';
|
|
17
|
+
export type { LiveExecutionContext, LiveInputs, LiveResourceExecutor, LiveScope } from './resource/types';
|
|
18
|
+
export { AllowAllAuthorizer, authKeysOf, isAuthKey } from './auth/authorizer';
|
|
19
|
+
export type { LiveAuthorizationRequest, LiveAuthorizer } from './auth/authorizer';
|
|
20
|
+
export { InProcessBus } from './bus/InProcessBus';
|
|
21
|
+
export type { InvalidationBus, InvalidationHandler } from './bus/InvalidationBus';
|
|
22
|
+
export type { Dependency, InvalidationEvent } from './graph/types';
|
|
23
|
+
export { ancestorsOf, rowKey, tableKey } from './graph/dep-key';
|
|
24
|
+
export type { DepKey } from './graph/dep-key';
|
|
25
|
+
export { WriteDuringComputeError } from './emitters/AppEmitter';
|
|
26
|
+
export { PgNotifyBus, chunkEvents } from './bus/PgNotifyBus';
|
|
27
|
+
export type { PgNotifyBusOptions } from './bus/PgNotifyBus';
|
|
28
|
+
export { PgNotifyEmitter, eventsFromPayload } from './emitters/pg-notify-emitter';
|
|
29
|
+
export type { PgNotifyEmitterOptions, PgNotifyTable } from './emitters/pg-notify-emitter';
|
|
30
|
+
export { PgListener } from './emitters/pg-listener';
|
|
31
|
+
export type { ListenableSql, PgListenerOptions } from './emitters/pg-listener';
|
|
32
|
+
export { tableOfKey } from './graph/dep-key';
|
|
33
|
+
export * from './shared/protocol';
|
|
34
|
+
export type { PatchOp, PathSegment } from './patch/types';
|
|
35
|
+
export { PatchEngine } from './patch/PatchEngine';
|
|
36
|
+
export { canonical, NonSerializableInputError } from './shared/canonical';
|
|
37
|
+
export { fnv1a64 } from './shared/hash';
|
|
38
|
+
export { normalizeLiveInputs, resourceIdOf } from './shared/descriptor';
|
|
39
|
+
export type { LiveDataOf, LiveDescriptor, LiveInputsOf } from './shared/descriptor';
|
|
40
|
+
export type { OptimisticEntry, OptimisticList } from './client/optimistic';
|
|
41
|
+
export { liveStore, liveStoreOf, liveIdentity, LiveSlot } from './client/vanilla';
|
|
42
|
+
export type { LiveHandle } from './client/vanilla';
|
|
43
|
+
export { WebSocketTransport, LadderTransport, PollingTransport, SseClientTransport, routeIndex } from './client/transport';
|
|
44
|
+
export type { ClientTransport, TransportHandlers, EventSourceLike, RoutePath } from './client/transport';
|
|
45
|
+
export { FanTransport } from './transport/FanTransport';
|
|
46
|
+
export type { OwnedTransport } from './transport/FanTransport';
|
|
47
|
+
export { SseTransport } from './transport/SseTransport';
|
|
48
|
+
export type { SseTransportOptions } from './transport/SseTransport';
|
|
49
|
+
export { createSseRoutes } from './transport/sse-routes';
|
|
50
|
+
export type { SseRouteOptions } from './transport/sse-routes';
|
|
51
|
+
export { LiveETagMiddleware, pathMatcher } from './http/etag';
|
|
52
|
+
export type { LivePollingGuard, LivePollingRequest, LiveRoutePath } from './http/etag';
|
|
53
|
+
export { prefetchLive } from './resource/prefetch';
|
|
54
|
+
export type { LivePayload } from './resource/prefetch';
|
|
55
|
+
export { LiveRouteExecutionError } from './resource/route-executor';
|
|
56
|
+
export { hydrationKey, toHydrateMap, readHydrationPayload, HYDRATION_ATTRIBUTE } from './client/hydrate';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.HYDRATION_ATTRIBUTE = exports.readHydrationPayload = exports.toHydrateMap = exports.hydrationKey = exports.LiveRouteExecutionError = exports.prefetchLive = exports.pathMatcher = exports.LiveETagMiddleware = exports.createSseRoutes = exports.SseTransport = exports.FanTransport = exports.routeIndex = exports.SseClientTransport = exports.PollingTransport = exports.LadderTransport = exports.WebSocketTransport = exports.LiveSlot = exports.liveIdentity = exports.liveStoreOf = exports.liveStore = exports.resourceIdOf = exports.normalizeLiveInputs = exports.fnv1a64 = exports.NonSerializableInputError = exports.canonical = exports.PatchEngine = exports.tableOfKey = exports.PgListener = exports.eventsFromPayload = exports.PgNotifyEmitter = exports.chunkEvents = exports.PgNotifyBus = exports.WriteDuringComputeError = exports.tableKey = exports.rowKey = exports.ancestorsOf = exports.InProcessBus = exports.isAuthKey = exports.authKeysOf = exports.AllowAllAuthorizer = exports.ConnectionScopeResolver = exports.resolveLiveConfig = exports.DEFAULT_LIVE_CONFIG = exports.LiveMetrics = exports.LiveEngine = exports.LiveService = exports.closeLiveRuntime = exports.LivePlugin = exports.LIVE_META = exports.Live = void 0;
|
|
18
|
+
require("reflect-metadata");
|
|
19
|
+
// Decorator and metadata
|
|
20
|
+
var Live_1 = require("./decorators/Live");
|
|
21
|
+
Object.defineProperty(exports, "Live", { enumerable: true, get: function () { return Live_1.Live; } });
|
|
22
|
+
var metadata_1 = require("./metadata");
|
|
23
|
+
Object.defineProperty(exports, "LIVE_META", { enumerable: true, get: function () { return metadata_1.LIVE_META; } });
|
|
24
|
+
// Plugin and services
|
|
25
|
+
var LivePlugin_1 = require("./LivePlugin");
|
|
26
|
+
Object.defineProperty(exports, "LivePlugin", { enumerable: true, get: function () { return LivePlugin_1.LivePlugin; } });
|
|
27
|
+
var runtime_1 = require("./runtime");
|
|
28
|
+
Object.defineProperty(exports, "closeLiveRuntime", { enumerable: true, get: function () { return runtime_1.closeLiveRuntime; } });
|
|
29
|
+
var LiveService_1 = require("./LiveService");
|
|
30
|
+
Object.defineProperty(exports, "LiveService", { enumerable: true, get: function () { return LiveService_1.LiveService; } });
|
|
31
|
+
var LiveEngine_1 = require("./LiveEngine");
|
|
32
|
+
Object.defineProperty(exports, "LiveEngine", { enumerable: true, get: function () { return LiveEngine_1.LiveEngine; } });
|
|
33
|
+
// Metrics
|
|
34
|
+
var observability_1 = require("./observability");
|
|
35
|
+
Object.defineProperty(exports, "LiveMetrics", { enumerable: true, get: function () { return observability_1.LiveMetrics; } });
|
|
36
|
+
// Configuration
|
|
37
|
+
var config_1 = require("./config");
|
|
38
|
+
Object.defineProperty(exports, "DEFAULT_LIVE_CONFIG", { enumerable: true, get: function () { return config_1.DEFAULT_LIVE_CONFIG; } });
|
|
39
|
+
Object.defineProperty(exports, "resolveLiveConfig", { enumerable: true, get: function () { return config_1.resolveLiveConfig; } });
|
|
40
|
+
// Scope
|
|
41
|
+
var scope_resolver_1 = require("./transport/scope-resolver");
|
|
42
|
+
Object.defineProperty(exports, "ConnectionScopeResolver", { enumerable: true, get: function () { return scope_resolver_1.ConnectionScopeResolver; } });
|
|
43
|
+
// Authorization
|
|
44
|
+
var authorizer_1 = require("./auth/authorizer");
|
|
45
|
+
Object.defineProperty(exports, "AllowAllAuthorizer", { enumerable: true, get: function () { return authorizer_1.AllowAllAuthorizer; } });
|
|
46
|
+
Object.defineProperty(exports, "authKeysOf", { enumerable: true, get: function () { return authorizer_1.authKeysOf; } });
|
|
47
|
+
Object.defineProperty(exports, "isAuthKey", { enumerable: true, get: function () { return authorizer_1.isAuthKey; } });
|
|
48
|
+
// Invalidation
|
|
49
|
+
var InProcessBus_1 = require("./bus/InProcessBus");
|
|
50
|
+
Object.defineProperty(exports, "InProcessBus", { enumerable: true, get: function () { return InProcessBus_1.InProcessBus; } });
|
|
51
|
+
var dep_key_1 = require("./graph/dep-key");
|
|
52
|
+
Object.defineProperty(exports, "ancestorsOf", { enumerable: true, get: function () { return dep_key_1.ancestorsOf; } });
|
|
53
|
+
Object.defineProperty(exports, "rowKey", { enumerable: true, get: function () { return dep_key_1.rowKey; } });
|
|
54
|
+
Object.defineProperty(exports, "tableKey", { enumerable: true, get: function () { return dep_key_1.tableKey; } });
|
|
55
|
+
var AppEmitter_1 = require("./emitters/AppEmitter");
|
|
56
|
+
Object.defineProperty(exports, "WriteDuringComputeError", { enumerable: true, get: function () { return AppEmitter_1.WriteDuringComputeError; } });
|
|
57
|
+
var PgNotifyBus_1 = require("./bus/PgNotifyBus");
|
|
58
|
+
Object.defineProperty(exports, "PgNotifyBus", { enumerable: true, get: function () { return PgNotifyBus_1.PgNotifyBus; } });
|
|
59
|
+
Object.defineProperty(exports, "chunkEvents", { enumerable: true, get: function () { return PgNotifyBus_1.chunkEvents; } });
|
|
60
|
+
var pg_notify_emitter_1 = require("./emitters/pg-notify-emitter");
|
|
61
|
+
Object.defineProperty(exports, "PgNotifyEmitter", { enumerable: true, get: function () { return pg_notify_emitter_1.PgNotifyEmitter; } });
|
|
62
|
+
Object.defineProperty(exports, "eventsFromPayload", { enumerable: true, get: function () { return pg_notify_emitter_1.eventsFromPayload; } });
|
|
63
|
+
var pg_listener_1 = require("./emitters/pg-listener");
|
|
64
|
+
Object.defineProperty(exports, "PgListener", { enumerable: true, get: function () { return pg_listener_1.PgListener; } });
|
|
65
|
+
var dep_key_2 = require("./graph/dep-key");
|
|
66
|
+
Object.defineProperty(exports, "tableOfKey", { enumerable: true, get: function () { return dep_key_2.tableOfKey; } });
|
|
67
|
+
// Protocol and patches, shared with the client
|
|
68
|
+
__exportStar(require("./shared/protocol"), exports);
|
|
69
|
+
var PatchEngine_1 = require("./patch/PatchEngine");
|
|
70
|
+
Object.defineProperty(exports, "PatchEngine", { enumerable: true, get: function () { return PatchEngine_1.PatchEngine; } });
|
|
71
|
+
var canonical_1 = require("./shared/canonical");
|
|
72
|
+
Object.defineProperty(exports, "canonical", { enumerable: true, get: function () { return canonical_1.canonical; } });
|
|
73
|
+
Object.defineProperty(exports, "NonSerializableInputError", { enumerable: true, get: function () { return canonical_1.NonSerializableInputError; } });
|
|
74
|
+
var hash_1 = require("./shared/hash");
|
|
75
|
+
Object.defineProperty(exports, "fnv1a64", { enumerable: true, get: function () { return hash_1.fnv1a64; } });
|
|
76
|
+
var descriptor_1 = require("./shared/descriptor");
|
|
77
|
+
Object.defineProperty(exports, "normalizeLiveInputs", { enumerable: true, get: function () { return descriptor_1.normalizeLiveInputs; } });
|
|
78
|
+
Object.defineProperty(exports, "resourceIdOf", { enumerable: true, get: function () { return descriptor_1.resourceIdOf; } });
|
|
79
|
+
// Framework-free client adapter
|
|
80
|
+
var vanilla_1 = require("./client/vanilla");
|
|
81
|
+
Object.defineProperty(exports, "liveStore", { enumerable: true, get: function () { return vanilla_1.liveStore; } });
|
|
82
|
+
Object.defineProperty(exports, "liveStoreOf", { enumerable: true, get: function () { return vanilla_1.liveStoreOf; } });
|
|
83
|
+
Object.defineProperty(exports, "liveIdentity", { enumerable: true, get: function () { return vanilla_1.liveIdentity; } });
|
|
84
|
+
Object.defineProperty(exports, "LiveSlot", { enumerable: true, get: function () { return vanilla_1.LiveSlot; } });
|
|
85
|
+
var transport_1 = require("./client/transport");
|
|
86
|
+
Object.defineProperty(exports, "WebSocketTransport", { enumerable: true, get: function () { return transport_1.WebSocketTransport; } });
|
|
87
|
+
Object.defineProperty(exports, "LadderTransport", { enumerable: true, get: function () { return transport_1.LadderTransport; } });
|
|
88
|
+
Object.defineProperty(exports, "PollingTransport", { enumerable: true, get: function () { return transport_1.PollingTransport; } });
|
|
89
|
+
Object.defineProperty(exports, "SseClientTransport", { enumerable: true, get: function () { return transport_1.SseClientTransport; } });
|
|
90
|
+
Object.defineProperty(exports, "routeIndex", { enumerable: true, get: function () { return transport_1.routeIndex; } });
|
|
91
|
+
// Transports
|
|
92
|
+
var FanTransport_1 = require("./transport/FanTransport");
|
|
93
|
+
Object.defineProperty(exports, "FanTransport", { enumerable: true, get: function () { return FanTransport_1.FanTransport; } });
|
|
94
|
+
var SseTransport_1 = require("./transport/SseTransport");
|
|
95
|
+
Object.defineProperty(exports, "SseTransport", { enumerable: true, get: function () { return SseTransport_1.SseTransport; } });
|
|
96
|
+
var sse_routes_1 = require("./transport/sse-routes");
|
|
97
|
+
Object.defineProperty(exports, "createSseRoutes", { enumerable: true, get: function () { return sse_routes_1.createSseRoutes; } });
|
|
98
|
+
// Conditional GET
|
|
99
|
+
var etag_1 = require("./http/etag");
|
|
100
|
+
Object.defineProperty(exports, "LiveETagMiddleware", { enumerable: true, get: function () { return etag_1.LiveETagMiddleware; } });
|
|
101
|
+
Object.defineProperty(exports, "pathMatcher", { enumerable: true, get: function () { return etag_1.pathMatcher; } });
|
|
102
|
+
// First paint
|
|
103
|
+
var prefetch_1 = require("./resource/prefetch");
|
|
104
|
+
Object.defineProperty(exports, "prefetchLive", { enumerable: true, get: function () { return prefetch_1.prefetchLive; } });
|
|
105
|
+
var route_executor_1 = require("./resource/route-executor");
|
|
106
|
+
Object.defineProperty(exports, "LiveRouteExecutionError", { enumerable: true, get: function () { return route_executor_1.LiveRouteExecutionError; } });
|
|
107
|
+
var hydrate_1 = require("./client/hydrate");
|
|
108
|
+
Object.defineProperty(exports, "hydrationKey", { enumerable: true, get: function () { return hydrate_1.hydrationKey; } });
|
|
109
|
+
Object.defineProperty(exports, "toHydrateMap", { enumerable: true, get: function () { return hydrate_1.toHydrateMap; } });
|
|
110
|
+
Object.defineProperty(exports, "readHydrationPayload", { enumerable: true, get: function () { return hydrate_1.readHydrationPayload; } });
|
|
111
|
+
Object.defineProperty(exports, "HYDRATION_ATTRIBUTE", { enumerable: true, get: function () { return hydrate_1.HYDRATION_ATTRIBUTE; } });
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export declare const LIVE_META: unique symbol;
|
|
2
|
+
export type LiveShared = 'private' | 'tenant' | 'public';
|
|
3
|
+
export interface LiveOptions {
|
|
4
|
+
/** Field that identifies a row of a returned collection. */
|
|
5
|
+
key?: string;
|
|
6
|
+
/** Who may share one computed instance. */
|
|
7
|
+
shared?: LiveShared;
|
|
8
|
+
/** Manual dependency keys for data the ORM cannot observe. */
|
|
9
|
+
dependsOn?: string[];
|
|
10
|
+
}
|
|
11
|
+
export interface LiveMeta {
|
|
12
|
+
key?: string;
|
|
13
|
+
shared: LiveShared;
|
|
14
|
+
dependsOn: string[];
|
|
15
|
+
handlerName: string;
|
|
16
|
+
}
|
package/dist/metadata.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The slice of `ObservabilityService` the live package needs.
|
|
3
|
+
*
|
|
4
|
+
* Declared structurally rather than imported so `@carno.js/core` stays an
|
|
5
|
+
* ordinary peer here and the metrics path is testable without one.
|
|
6
|
+
*/
|
|
7
|
+
export interface MetricSink {
|
|
8
|
+
onMetric(name: string, value: number, tags?: Record<string, string | number | boolean>): void;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Names the engine publishes, and the guard that keeps a broken metrics
|
|
12
|
+
* backend from breaking the engine.
|
|
13
|
+
*
|
|
14
|
+
* The engine calls this unconditionally; `none()` is what makes that safe when
|
|
15
|
+
* no observability plugin is installed, which is the default.
|
|
16
|
+
*/
|
|
17
|
+
export declare class LiveMetrics {
|
|
18
|
+
private readonly sink;
|
|
19
|
+
constructor(sink: MetricSink | null);
|
|
20
|
+
static none(): LiveMetrics;
|
|
21
|
+
recompute(resource: string, producedPatch: boolean, ops: number, durationMs: number): void;
|
|
22
|
+
invalidation(keys: number, fanout: number): void;
|
|
23
|
+
instances(count: number): void;
|
|
24
|
+
private publish;
|
|
25
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LiveMetrics = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Names the engine publishes, and the guard that keeps a broken metrics
|
|
6
|
+
* backend from breaking the engine.
|
|
7
|
+
*
|
|
8
|
+
* The engine calls this unconditionally; `none()` is what makes that safe when
|
|
9
|
+
* no observability plugin is installed, which is the default.
|
|
10
|
+
*/
|
|
11
|
+
class LiveMetrics {
|
|
12
|
+
constructor(sink) {
|
|
13
|
+
this.sink = sink;
|
|
14
|
+
}
|
|
15
|
+
static none() {
|
|
16
|
+
return new LiveMetrics(null);
|
|
17
|
+
}
|
|
18
|
+
recompute(resource, producedPatch, ops, durationMs) {
|
|
19
|
+
this.publish('live.recompute', 1, { resource, patched: producedPatch });
|
|
20
|
+
this.publish('live.recompute.ms', durationMs, { resource });
|
|
21
|
+
if (producedPatch) {
|
|
22
|
+
this.publish('live.patch.ops', ops, { resource });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
invalidation(keys, fanout) {
|
|
26
|
+
this.publish('live.invalidation.keys', keys);
|
|
27
|
+
this.publish('live.invalidation.fanout', fanout);
|
|
28
|
+
}
|
|
29
|
+
instances(count) {
|
|
30
|
+
this.publish('live.instances', count);
|
|
31
|
+
}
|
|
32
|
+
publish(name, value, tags) {
|
|
33
|
+
if (!this.sink) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
this.sink.onMetric(name, value, tags);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
// Losing a number is acceptable. Losing a recompute is not.
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
exports.LiveMetrics = LiveMetrics;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { PatchOp } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Snapshot → snapshot diffing, and patch application with structural sharing.
|
|
4
|
+
* Structural sharing keeps useSyncExternalStore snapshots referentially stable
|
|
5
|
+
* and lets framework memoization skip untouched branches.
|
|
6
|
+
*/
|
|
7
|
+
export declare class PatchEngine {
|
|
8
|
+
private readonly keyField?;
|
|
9
|
+
constructor(keyField?: string);
|
|
10
|
+
diff(prev: unknown, next: unknown): PatchOp[];
|
|
11
|
+
apply(prev: unknown, ops: PatchOp[]): unknown;
|
|
12
|
+
private diffValue;
|
|
13
|
+
private diffKeyedArray;
|
|
14
|
+
private isKeyed;
|
|
15
|
+
private keyOf;
|
|
16
|
+
private indexByKey;
|
|
17
|
+
private applyOne;
|
|
18
|
+
/** Rebuild only the containers along `path`, preserving all other refs. */
|
|
19
|
+
private replaceAt;
|
|
20
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PatchEngine = void 0;
|
|
4
|
+
function isPlainObject(value) {
|
|
5
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
6
|
+
return false;
|
|
7
|
+
}
|
|
8
|
+
const proto = Object.getPrototypeOf(value);
|
|
9
|
+
return proto === Object.prototype || proto === null;
|
|
10
|
+
}
|
|
11
|
+
function deepEqual(left, right) {
|
|
12
|
+
if (left === right) {
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
if (Array.isArray(left) && Array.isArray(right)) {
|
|
16
|
+
return left.length === right.length && left.every((item, i) => deepEqual(item, right[i]));
|
|
17
|
+
}
|
|
18
|
+
if (isPlainObject(left) && isPlainObject(right)) {
|
|
19
|
+
const leftKeys = Object.keys(left);
|
|
20
|
+
if (leftKeys.length !== Object.keys(right).length) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
return leftKeys.every(key => Object.prototype.hasOwnProperty.call(right, key) && deepEqual(left[key], right[key]));
|
|
24
|
+
}
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Snapshot → snapshot diffing, and patch application with structural sharing.
|
|
29
|
+
* Structural sharing keeps useSyncExternalStore snapshots referentially stable
|
|
30
|
+
* and lets framework memoization skip untouched branches.
|
|
31
|
+
*/
|
|
32
|
+
class PatchEngine {
|
|
33
|
+
constructor(keyField) {
|
|
34
|
+
this.keyField = keyField;
|
|
35
|
+
}
|
|
36
|
+
diff(prev, next) {
|
|
37
|
+
const ops = [];
|
|
38
|
+
this.diffValue(prev, next, [], ops);
|
|
39
|
+
return ops;
|
|
40
|
+
}
|
|
41
|
+
apply(prev, ops) {
|
|
42
|
+
if (ops.length === 0) {
|
|
43
|
+
return prev;
|
|
44
|
+
}
|
|
45
|
+
let root = prev;
|
|
46
|
+
for (const op of ops) {
|
|
47
|
+
root = this.applyOne(root, op);
|
|
48
|
+
}
|
|
49
|
+
return root;
|
|
50
|
+
}
|
|
51
|
+
diffValue(prev, next, path, ops) {
|
|
52
|
+
if (prev === next) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (Array.isArray(prev) && Array.isArray(next)) {
|
|
56
|
+
if (this.isKeyed(prev) && this.isKeyed(next)) {
|
|
57
|
+
this.diffKeyedArray(prev, next, path, ops);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (!deepEqual(prev, next)) {
|
|
61
|
+
ops.push({ op: 'set', path, value: next });
|
|
62
|
+
}
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (isPlainObject(prev) && isPlainObject(next)) {
|
|
66
|
+
for (const key of Object.keys(prev)) {
|
|
67
|
+
if (!Object.prototype.hasOwnProperty.call(next, key)) {
|
|
68
|
+
ops.push({ op: 'unset', path: [...path, key] });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
for (const key of Object.keys(next)) {
|
|
72
|
+
if (!Object.prototype.hasOwnProperty.call(prev, key)) {
|
|
73
|
+
ops.push({ op: 'set', path: [...path, key], value: next[key] });
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
this.diffValue(prev[key], next[key], [...path, key], ops);
|
|
77
|
+
}
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (!deepEqual(prev, next)) {
|
|
81
|
+
ops.push({ op: 'set', path, value: next });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
diffKeyedArray(prev, next, path, ops) {
|
|
85
|
+
const prevByKey = this.indexByKey(prev);
|
|
86
|
+
const nextByKey = this.indexByKey(next);
|
|
87
|
+
for (const key of prevByKey.keys()) {
|
|
88
|
+
if (!nextByKey.has(key)) {
|
|
89
|
+
ops.push({ op: 'remove', path, key });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
next.forEach((row, index) => {
|
|
93
|
+
const key = this.keyOf(row);
|
|
94
|
+
const before = prevByKey.get(key);
|
|
95
|
+
if (before === undefined || !deepEqual(before, row)) {
|
|
96
|
+
ops.push({ op: 'upsert', path, key, index, value: row });
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
const survivingPrevKeys = [...prevByKey.keys()].filter(key => nextByKey.has(key));
|
|
100
|
+
const nextKeys = [...nextByKey.keys()];
|
|
101
|
+
const orderChanged = survivingPrevKeys.length !== nextKeys.length ||
|
|
102
|
+
survivingPrevKeys.some((key, i) => key !== nextKeys[i]);
|
|
103
|
+
if (orderChanged) {
|
|
104
|
+
ops.push({ op: 'order', path, keys: nextKeys });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
isKeyed(value) {
|
|
108
|
+
if (!this.keyField) {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
return value.every(item => this.keyOf(item) !== undefined);
|
|
112
|
+
}
|
|
113
|
+
keyOf(row) {
|
|
114
|
+
if (!this.keyField || !isPlainObject(row)) {
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
const value = row[this.keyField];
|
|
118
|
+
return typeof value === 'string' || typeof value === 'number' ? value : undefined;
|
|
119
|
+
}
|
|
120
|
+
indexByKey(rows) {
|
|
121
|
+
const index = new Map();
|
|
122
|
+
for (const row of rows) {
|
|
123
|
+
index.set(this.keyOf(row), row);
|
|
124
|
+
}
|
|
125
|
+
return index;
|
|
126
|
+
}
|
|
127
|
+
applyOne(root, op) {
|
|
128
|
+
if (op.op === 'set') {
|
|
129
|
+
return this.replaceAt(root, op.path, () => op.value);
|
|
130
|
+
}
|
|
131
|
+
if (op.op === 'unset') {
|
|
132
|
+
const parentPath = op.path.slice(0, -1);
|
|
133
|
+
const key = op.path[op.path.length - 1];
|
|
134
|
+
return this.replaceAt(root, parentPath, current => {
|
|
135
|
+
if (!isPlainObject(current)) {
|
|
136
|
+
return current;
|
|
137
|
+
}
|
|
138
|
+
const clone = { ...current };
|
|
139
|
+
delete clone[String(key)];
|
|
140
|
+
return clone;
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
return this.replaceAt(root, op.path, current => {
|
|
144
|
+
const rows = Array.isArray(current) ? current : [];
|
|
145
|
+
if (op.op === 'remove') {
|
|
146
|
+
return rows.filter(row => this.keyOf(row) !== op.key);
|
|
147
|
+
}
|
|
148
|
+
if (op.op === 'upsert') {
|
|
149
|
+
const index = rows.findIndex(row => this.keyOf(row) === op.key);
|
|
150
|
+
if (index === -1) {
|
|
151
|
+
const clone = rows.slice();
|
|
152
|
+
clone.splice(Math.min(op.index, clone.length), 0, op.value);
|
|
153
|
+
return clone;
|
|
154
|
+
}
|
|
155
|
+
const clone = rows.slice();
|
|
156
|
+
clone[index] = op.value;
|
|
157
|
+
return clone;
|
|
158
|
+
}
|
|
159
|
+
const byKey = new Map(rows.map(row => [this.keyOf(row), row]));
|
|
160
|
+
return op.keys.map(key => byKey.get(key)).filter(row => row !== undefined);
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
/** Rebuild only the containers along `path`, preserving all other refs. */
|
|
164
|
+
replaceAt(root, path, update) {
|
|
165
|
+
if (path.length === 0) {
|
|
166
|
+
return update(root);
|
|
167
|
+
}
|
|
168
|
+
const [head, ...rest] = path;
|
|
169
|
+
if (Array.isArray(root)) {
|
|
170
|
+
const index = Number(head);
|
|
171
|
+
const clone = root.slice();
|
|
172
|
+
clone[index] = this.replaceAt(root[index], rest, update);
|
|
173
|
+
return clone;
|
|
174
|
+
}
|
|
175
|
+
const base = isPlainObject(root) ? root : {};
|
|
176
|
+
const key = String(head);
|
|
177
|
+
return { ...base, [key]: this.replaceAt(base[key], rest, update) };
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
exports.PatchEngine = PatchEngine;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export type PathSegment = string | number;
|
|
2
|
+
/** Replace the value at `path`. */
|
|
3
|
+
export interface SetOp {
|
|
4
|
+
op: 'set';
|
|
5
|
+
path: PathSegment[];
|
|
6
|
+
value: unknown;
|
|
7
|
+
}
|
|
8
|
+
/** Delete the property at `path`. */
|
|
9
|
+
export interface UnsetOp {
|
|
10
|
+
op: 'unset';
|
|
11
|
+
path: PathSegment[];
|
|
12
|
+
}
|
|
13
|
+
/** Insert or replace one row of a keyed array at `path`. */
|
|
14
|
+
export interface UpsertOp {
|
|
15
|
+
op: 'upsert';
|
|
16
|
+
path: PathSegment[];
|
|
17
|
+
key: string | number;
|
|
18
|
+
index: number;
|
|
19
|
+
value: unknown;
|
|
20
|
+
}
|
|
21
|
+
/** Remove one row, by key, from a keyed array at `path`. */
|
|
22
|
+
export interface RemoveOp {
|
|
23
|
+
op: 'remove';
|
|
24
|
+
path: PathSegment[];
|
|
25
|
+
key: string | number;
|
|
26
|
+
}
|
|
27
|
+
/** Final key order of a keyed array at `path`. */
|
|
28
|
+
export interface OrderOp {
|
|
29
|
+
op: 'order';
|
|
30
|
+
path: PathSegment[];
|
|
31
|
+
keys: (string | number)[];
|
|
32
|
+
}
|
|
33
|
+
export type PatchOp = SetOp | UnsetOp | UpsertOp | RemoveOp | OrderOp;
|