@swirls/looms 0.4.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +61 -0
- package/dist/actor/cell.d.ts +21 -2
- package/dist/actor/cell.js +15 -3
- package/dist/actor/host.d.ts +10 -2
- package/dist/actor/host.js +27 -4
- package/dist/actor/index.d.ts +2 -1
- package/dist/actor/index.js +1 -1
- package/dist/actor/routing.d.ts +3 -0
- package/dist/actor/routing.js +13 -2
- package/dist/agent/definitions-store.d.ts +1 -1
- package/dist/agent/definitions-store.js +1 -1
- package/dist/agent/definitions.d.ts +48 -0
- package/dist/agent/definitions.js +44 -0
- package/dist/agent/effects.d.ts +184 -0
- package/dist/agent/effects.js +172 -40
- package/dist/agent/events.d.ts +89 -0
- package/dist/agent/events.js +25 -0
- package/dist/agent/module.d.ts +412 -0
- package/dist/agent/module.js +5 -1
- package/dist/agent/scope.d.ts +88 -0
- package/dist/agent/threads.js +39 -3
- package/dist/agent/types.d.ts +4 -0
- package/dist/approval/effects.d.ts +40 -0
- package/dist/approval/module.d.ts +80 -0
- package/dist/client/client.d.ts +468 -119
- package/dist/client/client.js +214 -21
- package/dist/client/index.d.ts +1 -1
- package/dist/client/sse.d.ts +1 -1
- package/dist/client/sse.js +2 -2
- package/dist/cloudflare/durable-object.d.ts +66 -2
- package/dist/cloudflare/durable-object.js +96 -1
- package/dist/cloudflare/event-store.d.ts +7 -3
- package/dist/cloudflare/event-store.js +16 -3
- package/dist/cloudflare/index.d.ts +7 -1
- package/dist/cloudflare/index.js +7 -1
- package/dist/cloudflare/memory-queue.d.ts +18 -0
- package/dist/cloudflare/memory-queue.js +53 -0
- package/dist/cloudflare/queue-consumer.d.ts +52 -0
- package/dist/cloudflare/queue-consumer.js +102 -0
- package/dist/cloudflare/queue-message.d.ts +31 -0
- package/dist/cloudflare/queue-message.js +33 -0
- package/dist/cloudflare/queue-pull.d.ts +62 -0
- package/dist/cloudflare/queue-pull.js +130 -0
- package/dist/cloudflare/queue-worker.d.ts +39 -0
- package/dist/cloudflare/queue-worker.js +62 -0
- package/dist/cloudflare/queues.d.ts +10 -0
- package/dist/cloudflare/queues.js +10 -0
- package/dist/cloudflare/r2-blob-store.d.ts +33 -0
- package/dist/cloudflare/r2-blob-store.js +60 -0
- package/dist/cloudflare/router.d.ts +4 -1
- package/dist/cloudflare/router.js +8 -3
- package/dist/cloudflare/websocket.d.ts +27 -0
- package/dist/cloudflare/websocket.js +215 -0
- package/dist/core/blob-offload.d.ts +21 -0
- package/dist/core/blob-offload.js +143 -0
- package/dist/core/blob-store.d.ts +45 -0
- package/dist/core/blob-store.js +164 -0
- package/dist/core/blobs.d.ts +37 -0
- package/dist/core/blobs.js +160 -0
- package/dist/core/definition-resolver.d.ts +40 -0
- package/dist/core/definition-resolver.js +129 -0
- package/dist/core/definition-store.d.ts +6 -0
- package/dist/core/definition-store.js +37 -1
- package/dist/core/effects.d.ts +86 -3
- package/dist/core/effects.js +99 -3
- package/dist/core/envelope.d.ts +9 -0
- package/dist/core/envelope.js +4 -0
- package/dist/core/event-codec.js +3 -0
- package/dist/core/fold.js +83 -1
- package/dist/core/ids.d.ts +2 -0
- package/dist/core/ids.js +4 -0
- package/dist/core/index.d.ts +8 -0
- package/dist/core/index.js +8 -0
- package/dist/core/launcher.d.ts +97 -0
- package/dist/core/launcher.js +67 -0
- package/dist/core/module-scope.d.ts +7 -3
- package/dist/core/module-scope.js +3 -0
- package/dist/core/module.d.ts +10 -0
- package/dist/core/projection.js +4 -0
- package/dist/core/protocol.d.ts +36 -1
- package/dist/core/protocol.js +35 -0
- package/dist/core/published.d.ts +2 -0
- package/dist/core/published.js +4 -1
- package/dist/core/replay.d.ts +96 -0
- package/dist/core/run-context.d.ts +33 -0
- package/dist/core/run-context.js +23 -0
- package/dist/core/sqlite-store.js +57 -5
- package/dist/core/state.d.ts +60 -0
- package/dist/core/state.js +19 -0
- package/dist/core/store.d.ts +6 -0
- package/dist/core/store.js +7 -0
- package/dist/core/subscription.d.ts +97 -0
- package/dist/core/subscription.js +224 -0
- package/dist/core/thread.d.ts +7 -0
- package/dist/core/thread.js +1 -0
- package/dist/core/trace.d.ts +21 -0
- package/dist/core/trace.js +45 -0
- package/dist/core/worker-callback.d.ts +25 -1
- package/dist/debugger/app/assets/{index-CgDzWJg7.css → index-B7GdT1UA.css} +1 -1
- package/dist/debugger/app/assets/index-C31SnCuN.js +22 -0
- package/dist/debugger/app/assets/routes-DPA3O5AF.js +47 -0
- package/dist/debugger/app/index.html +2 -2
- package/dist/debugger/components/event-inspector.d.ts +3 -1
- package/dist/debugger/components/event-inspector.js +25 -4
- package/dist/debugger/components/event-stream.d.ts +4 -1
- package/dist/debugger/components/event-stream.js +2 -2
- package/dist/debugger/components/run-tree.d.ts +5 -2
- package/dist/debugger/components/run-tree.js +10 -4
- package/dist/debugger/context.d.ts +8 -1
- package/dist/debugger/context.js +15 -3
- package/dist/debugger/events/protocol-summarize.js +18 -3
- package/dist/debugger/index.d.ts +1 -1
- package/dist/debugger/index.js +1 -1
- package/dist/debugger/shell/debugger-shell.js +1 -1
- package/dist/debugger/shell/events-pane.js +4 -3
- package/dist/debugger/styles.css +1 -0
- package/dist/evaluator/definitions-store.d.ts +3 -4
- package/dist/evaluator/definitions-store.js +3 -3
- package/dist/evaluator/definitions.d.ts +4 -1
- package/dist/evaluator/definitions.js +5 -2
- package/dist/evaluator/effects.d.ts +41 -0
- package/dist/evaluator/effects.js +6 -2
- package/dist/evaluator/module.d.ts +84 -0
- package/dist/evaluator/module.js +7 -3
- package/dist/evaluator/threads.d.ts +2 -0
- package/dist/evaluator/threads.js +4 -0
- package/dist/projectors/event-log.d.ts +87 -0
- package/dist/projectors/event-log.js +396 -0
- package/dist/projectors/index-model.d.ts +12 -0
- package/dist/projectors/index-model.js +12 -3
- package/dist/projectors/memory.js +7 -0
- package/dist/projectors/postgres.d.ts +1 -0
- package/dist/projectors/postgres.js +32 -15
- package/dist/projectors/schema.d.ts +5 -0
- package/dist/projectors/schema.js +26 -1
- package/dist/projectors/sqlite.js +47 -19
- package/dist/projectors/with-projectors.d.ts +7 -1
- package/dist/projectors/with-projectors.js +13 -3
- package/dist/react/materialize.js +5 -1
- package/dist/react/react.d.ts +10 -2
- package/dist/react/react.js +43 -19
- package/dist/react/store.d.ts +11 -2
- package/dist/react/store.js +104 -12
- package/dist/react/tables.d.ts +4 -1
- package/dist/runtime/cancellation.d.ts +2 -1
- package/dist/runtime/cancellation.js +35 -2
- package/dist/runtime/child-run.d.ts +10 -0
- package/dist/runtime/child-run.js +58 -0
- package/dist/runtime/deadline-transition-planner.js +11 -2
- package/dist/runtime/dispatch-effect.d.ts +2 -2
- package/dist/runtime/dispatch-effect.js +43 -19
- package/dist/runtime/errors.d.ts +14 -1
- package/dist/runtime/errors.js +10 -0
- package/dist/runtime/filesystem-blob-store.d.ts +6 -0
- package/dist/runtime/filesystem-blob-store.js +59 -0
- package/dist/runtime/helpers.d.ts +15 -6
- package/dist/runtime/helpers.js +46 -6
- package/dist/runtime/index.d.ts +4 -2
- package/dist/runtime/index.js +3 -1
- package/dist/runtime/local-effect-executor.d.ts +11 -2
- package/dist/runtime/local-effect-executor.js +80 -4
- package/dist/runtime/looms.d.ts +38 -4
- package/dist/runtime/looms.js +17 -3
- package/dist/runtime/repository.d.ts +5 -4
- package/dist/runtime/repository.js +5 -2
- package/dist/runtime/retry-policy.d.ts +5 -0
- package/dist/runtime/retry-policy.js +10 -2
- package/dist/runtime/run-start.d.ts +1 -1
- package/dist/runtime/run-start.js +10 -0
- package/dist/runtime/runtime.d.ts +2 -2
- package/dist/runtime/runtime.js +191 -68
- package/dist/runtime/server/auth.d.ts +3 -0
- package/dist/runtime/server/fetch-handler.d.ts +2 -2
- package/dist/runtime/server/fetch-handler.js +35 -4
- package/dist/runtime/server/grant.d.ts +49 -0
- package/dist/runtime/server/grant.js +177 -0
- package/dist/runtime/server/routes/effects.js +47 -5
- package/dist/runtime/server/routes/runs.js +6 -1
- package/dist/runtime/server/run-route.d.ts +18 -0
- package/dist/runtime/server/run-route.js +26 -0
- package/dist/runtime/server.d.ts +3 -1
- package/dist/runtime/server.js +3 -1
- package/dist/runtime/sse.js +26 -4
- package/dist/runtime/subscription.d.ts +8 -0
- package/dist/runtime/subscription.js +23 -0
- package/dist/runtime/types.d.ts +87 -8
- package/dist/runtime/wake-engine.d.ts +4 -2
- package/dist/runtime/wake-engine.js +57 -6
- package/dist/runtime/worker-callbacks.d.ts +12 -2
- package/dist/runtime/worker-callbacks.js +102 -72
- package/dist/runtime/worker-effect-dispatch.d.ts +14 -5
- package/dist/runtime/worker-effect-dispatch.js +84 -18
- package/dist/schedule/debugger/index.d.ts +4 -0
- package/dist/schedule/debugger/index.js +11 -0
- package/dist/schedule/debugger/schedule-view.d.ts +3 -0
- package/dist/schedule/debugger/schedule-view.js +68 -0
- package/dist/schedule/debugger/summarize.d.ts +2 -0
- package/dist/schedule/debugger/summarize.js +43 -0
- package/dist/schedule/definitions-store.d.ts +10 -0
- package/dist/schedule/definitions-store.js +8 -0
- package/dist/schedule/definitions.d.ts +57 -0
- package/dist/schedule/definitions.js +30 -0
- package/dist/schedule/effects.d.ts +756 -0
- package/dist/schedule/effects.js +244 -0
- package/dist/schedule/ensure.d.ts +55 -0
- package/dist/schedule/ensure.js +109 -0
- package/dist/schedule/events.d.ts +116 -0
- package/dist/schedule/events.js +49 -0
- package/dist/schedule/index.d.ts +11 -0
- package/dist/schedule/index.js +11 -0
- package/dist/schedule/module.d.ts +1474 -0
- package/dist/schedule/module.js +22 -0
- package/dist/schedule/projections.d.ts +55 -0
- package/dist/schedule/projections.js +88 -0
- package/dist/schedule/scope.d.ts +121 -0
- package/dist/schedule/scope.js +7 -0
- package/dist/schedule/signals.d.ts +25 -0
- package/dist/schedule/signals.js +33 -0
- package/dist/schedule/specs.d.ts +59 -0
- package/dist/schedule/specs.js +231 -0
- package/dist/schedule/threads.d.ts +25 -0
- package/dist/schedule/threads.js +259 -0
- package/dist/testing/corpus-module.d.ts +34 -0
- package/dist/testing/corpus-module.js +87 -0
- package/dist/testing/index.d.ts +2 -0
- package/dist/testing/index.js +5 -6
- package/dist/testing/replay-corpus.d.ts +22 -0
- package/dist/testing/replay-corpus.js +50 -0
- package/dist/workflow/definitions-store.d.ts +1 -1
- package/dist/workflow/definitions-store.js +1 -1
- package/dist/workflow/definitions.d.ts +14 -0
- package/dist/workflow/effects.d.ts +122 -0
- package/dist/workflow/effects.js +16 -6
- package/dist/workflow/events.d.ts +42 -0
- package/dist/workflow/events.js +4 -0
- package/dist/workflow/module.d.ts +313 -0
- package/dist/workflow/module.js +7 -3
- package/dist/workflow/scope.d.ts +42 -0
- package/dist/workflow/threads.d.ts +72 -0
- package/dist/workflow/threads.js +106 -38
- package/package.json +14 -1
- package/dist/debugger/app/assets/index-D--9esm9.js +0 -22
- package/dist/debugger/app/assets/routes-Dakrdusf.js +0 -47
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { Data, Effect, Schema } from 'effect';
|
|
2
|
+
const SHA256 = /^[a-f0-9]{64}$/;
|
|
3
|
+
const decodeDigestList = Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Array(Schema.String)));
|
|
4
|
+
export class BlobStoreError extends Data.TaggedError('BlobStoreError') {
|
|
5
|
+
cause;
|
|
6
|
+
constructor(message, cause) {
|
|
7
|
+
super({ message, cause });
|
|
8
|
+
this.name = 'BlobStoreError';
|
|
9
|
+
this.cause = cause;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export class BlobNotFoundError extends Data.TaggedError('BlobNotFoundError') {
|
|
13
|
+
constructor(digest) {
|
|
14
|
+
super({ digest, message: `blob ${digest} was not found` });
|
|
15
|
+
this.name = 'BlobNotFoundError';
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export function isSha256Digest(value) {
|
|
19
|
+
return SHA256.test(value);
|
|
20
|
+
}
|
|
21
|
+
const EMPTY = new Uint8Array(0);
|
|
22
|
+
export function memoryBlobStore() {
|
|
23
|
+
return blobStoreFromBytes(memoryBlobBytes());
|
|
24
|
+
}
|
|
25
|
+
export function blobStoreFromBytes(bytes) {
|
|
26
|
+
return {
|
|
27
|
+
put: (digest, body) => Effect.gen(function* () {
|
|
28
|
+
yield* assertDigest(digest);
|
|
29
|
+
const actual = yield* sha256Hex(body);
|
|
30
|
+
if (actual !== digest) {
|
|
31
|
+
return yield* new BlobStoreError(`blob body does not match digest ${digest}`);
|
|
32
|
+
}
|
|
33
|
+
const existing = yield* bytes.read(objectKey(digest));
|
|
34
|
+
if (existing) {
|
|
35
|
+
return yield* Effect.void;
|
|
36
|
+
}
|
|
37
|
+
return yield* bytes.write(objectKey(digest), body);
|
|
38
|
+
}),
|
|
39
|
+
get: (digest) => Effect.gen(function* () {
|
|
40
|
+
yield* assertDigest(digest);
|
|
41
|
+
const body = yield* bytes.read(objectKey(digest));
|
|
42
|
+
if (!body) {
|
|
43
|
+
return yield* new BlobNotFoundError(digest);
|
|
44
|
+
}
|
|
45
|
+
const actual = yield* sha256Hex(body);
|
|
46
|
+
if (actual !== digest) {
|
|
47
|
+
return yield* new BlobStoreError(`blob ${digest} failed its integrity check`);
|
|
48
|
+
}
|
|
49
|
+
return body;
|
|
50
|
+
}),
|
|
51
|
+
// Markers go down before the existence check, and `collect` lists markers again after
|
|
52
|
+
// deleting. Whichever side runs second sees the other, so a referenced body is never lost:
|
|
53
|
+
// either `collect` restores it or this append fails and its retry uploads the body again.
|
|
54
|
+
retain: (runId, digests) => Effect.gen(function* () {
|
|
55
|
+
const current = new Set(yield* readRunDigests(bytes, runId));
|
|
56
|
+
const added = [...new Set(digests)].filter((digest) => !current.has(digest));
|
|
57
|
+
if (added.length === 0) {
|
|
58
|
+
return yield* Effect.void;
|
|
59
|
+
}
|
|
60
|
+
yield* Effect.forEach(added, assertDigest, { discard: true });
|
|
61
|
+
yield* Effect.forEach(added, (digest) => bytes.write(refKey(digest, runId), EMPTY), {
|
|
62
|
+
concurrency: REF_CONCURRENCY,
|
|
63
|
+
discard: true,
|
|
64
|
+
});
|
|
65
|
+
yield* writeRunDigests(bytes, runId, [...current, ...added]);
|
|
66
|
+
const present = yield* Effect.forEach(added, (digest) => bytes.exists(objectKey(digest)), {
|
|
67
|
+
concurrency: REF_CONCURRENCY,
|
|
68
|
+
});
|
|
69
|
+
const missing = added.filter((_, index) => !present[index]);
|
|
70
|
+
if (missing.length > 0) {
|
|
71
|
+
return yield* new BlobStoreError(`blobs ${missing.join(', ')} were deleted before run ${runId} referenced them; retry the append`);
|
|
72
|
+
}
|
|
73
|
+
return yield* Effect.void;
|
|
74
|
+
}),
|
|
75
|
+
replaceRefs: (runId, keep) => Effect.gen(function* () {
|
|
76
|
+
const previous = yield* readRunDigests(bytes, runId);
|
|
77
|
+
const dropped = previous.filter((digest) => !keep.has(digest));
|
|
78
|
+
if (dropped.length === 0) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
yield* Effect.forEach(dropped, (digest) => bytes.remove(refKey(digest, runId)).pipe(Effect.andThen(collect(bytes, digest))), { concurrency: REF_CONCURRENCY, discard: true });
|
|
82
|
+
yield* writeRunDigests(bytes, runId, previous.filter((digest) => keep.has(digest)));
|
|
83
|
+
}),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
const REF_CONCURRENCY = 8;
|
|
87
|
+
/** Deletes a body no run references, and puts it back if a run referenced it meanwhile. */
|
|
88
|
+
function collect(bytes, digest) {
|
|
89
|
+
const referenced = bytes.list(refPrefix(digest)).pipe(Effect.map((keys) => keys.length > 0));
|
|
90
|
+
return Effect.gen(function* () {
|
|
91
|
+
if (yield* referenced) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const body = yield* bytes.read(objectKey(digest));
|
|
95
|
+
if (!body) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
yield* bytes.remove(objectKey(digest));
|
|
99
|
+
if (yield* referenced) {
|
|
100
|
+
yield* bytes.write(objectKey(digest), body);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
function memoryBlobBytes() {
|
|
105
|
+
const objects = new Map();
|
|
106
|
+
return {
|
|
107
|
+
read: (key) => Effect.sync(() => {
|
|
108
|
+
const body = objects.get(key);
|
|
109
|
+
return body ? body.slice() : null;
|
|
110
|
+
}),
|
|
111
|
+
write: (key, body) => Effect.sync(() => {
|
|
112
|
+
objects.set(key, body.slice());
|
|
113
|
+
}),
|
|
114
|
+
remove: (key) => Effect.sync(() => {
|
|
115
|
+
objects.delete(key);
|
|
116
|
+
}),
|
|
117
|
+
exists: (key) => Effect.sync(() => objects.has(key)),
|
|
118
|
+
list: (prefix) => Effect.sync(() => [...objects.keys()].filter((key) => key.startsWith(prefix))),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
function objectKey(digest) {
|
|
122
|
+
return `objects/${digest}`;
|
|
123
|
+
}
|
|
124
|
+
function runKey(runId) {
|
|
125
|
+
return `runs/${encodeURIComponent(runId)}`;
|
|
126
|
+
}
|
|
127
|
+
function refPrefix(digest) {
|
|
128
|
+
return `refs/${digest}/`;
|
|
129
|
+
}
|
|
130
|
+
function refKey(digest, runId) {
|
|
131
|
+
return `${refPrefix(digest)}${encodeURIComponent(runId)}`;
|
|
132
|
+
}
|
|
133
|
+
function assertDigest(digest) {
|
|
134
|
+
if (isSha256Digest(digest)) {
|
|
135
|
+
return Effect.void;
|
|
136
|
+
}
|
|
137
|
+
return Effect.fail(new BlobStoreError(`invalid blob digest ${digest}`));
|
|
138
|
+
}
|
|
139
|
+
export function sha256Hex(body) {
|
|
140
|
+
const copy = new Uint8Array(body.byteLength);
|
|
141
|
+
copy.set(body);
|
|
142
|
+
return Effect.tryPromise({
|
|
143
|
+
try: () => crypto.subtle.digest('SHA-256', copy),
|
|
144
|
+
catch: (cause) => new BlobStoreError('SHA-256 failed', cause),
|
|
145
|
+
}).pipe(Effect.map((buffer) => hex(new Uint8Array(buffer))));
|
|
146
|
+
}
|
|
147
|
+
function hex(bytes) {
|
|
148
|
+
let encoded = '';
|
|
149
|
+
for (const byte of bytes) {
|
|
150
|
+
encoded += byte.toString(16).padStart(2, '0');
|
|
151
|
+
}
|
|
152
|
+
return encoded;
|
|
153
|
+
}
|
|
154
|
+
function readRunDigests(bytes, runId) {
|
|
155
|
+
return bytes.read(runKey(runId)).pipe(Effect.flatMap((body) => {
|
|
156
|
+
if (!body) {
|
|
157
|
+
return Effect.succeed([]);
|
|
158
|
+
}
|
|
159
|
+
return decodeDigestList(new TextDecoder().decode(body)).pipe(Effect.mapError((cause) => new BlobStoreError(`blob references for ${runId} are unreadable`, cause)));
|
|
160
|
+
}));
|
|
161
|
+
}
|
|
162
|
+
function writeRunDigests(bytes, runId, digests) {
|
|
163
|
+
return bytes.write(runKey(runId), new TextEncoder().encode(JSON.stringify(digests)));
|
|
164
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Effect } from 'effect';
|
|
2
|
+
import { type BlobStore, type BlobStoreFailure } from './blob-store';
|
|
3
|
+
import { type JsonValue } from './types';
|
|
4
|
+
export declare const DEFAULT_BLOB_THRESHOLD_BYTES: number;
|
|
5
|
+
declare const BLOB_KIND = "blob";
|
|
6
|
+
declare const BLOB_MEDIA_TYPE = "application/json";
|
|
7
|
+
/**
|
|
8
|
+
* Reserved stored shape. A JSON object with exactly these fields is a claim check,
|
|
9
|
+
* not caller data. `bytes` is the UTF-8 length of the JSON body named by `sha256`.
|
|
10
|
+
*/
|
|
11
|
+
export interface BlobRef {
|
|
12
|
+
readonly $looms: typeof BLOB_KIND;
|
|
13
|
+
readonly sha256: string;
|
|
14
|
+
readonly bytes: number;
|
|
15
|
+
readonly mediaType: typeof BLOB_MEDIA_TYPE;
|
|
16
|
+
readonly [key: string]: JsonValue;
|
|
17
|
+
}
|
|
18
|
+
export declare function isBlobRef(value: JsonValue): value is BlobRef;
|
|
19
|
+
export interface OffloadJsonOptions {
|
|
20
|
+
readonly thresholdBytes?: number;
|
|
21
|
+
/** When false, the root value stays inline even if it is over the threshold. Children are still claimed. */
|
|
22
|
+
readonly claimRoot?: boolean;
|
|
23
|
+
}
|
|
24
|
+
/** Store `value` and return a reference, even when it is under the offload threshold. */
|
|
25
|
+
export declare function claimJson(value: JsonValue, blobs: BlobStore): Effect.Effect<BlobRef, BlobStoreFailure>;
|
|
26
|
+
/** Replace values larger than `thresholdBytes` with {@link BlobRef}s. Bodies are stored by hash. */
|
|
27
|
+
export declare function offloadJson(value: JsonValue, blobs: BlobStore, options?: OffloadJsonOptions): Effect.Effect<JsonValue, BlobStoreFailure>;
|
|
28
|
+
/** Replace {@link BlobRef}s with the JSON they name, including references nested inside a body. */
|
|
29
|
+
export declare function resolveJson(value: JsonValue, blobs: BlobStore): Effect.Effect<JsonValue, BlobStoreFailure>;
|
|
30
|
+
/**
|
|
31
|
+
* Digests named by `value`, plus digests nested inside those bodies.
|
|
32
|
+
* A parent blob that itself contains references still keeps those bodies alive.
|
|
33
|
+
*/
|
|
34
|
+
export declare function expandBlobDigests(blobs: BlobStore, seeds: ReadonlySet<string>): Effect.Effect<ReadonlySet<string>, BlobStoreFailure>;
|
|
35
|
+
/** Record every blob digest embedded in `value`. Does not read blob bodies. */
|
|
36
|
+
export declare function collectBlobDigests(value: JsonValue, into: Set<string>): void;
|
|
37
|
+
export {};
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { Effect, Predicate, Schema } from 'effect';
|
|
2
|
+
import { BlobStoreError, isSha256Digest, sha256Hex, } from './blob-store.js';
|
|
3
|
+
import { utf8JsonBytes } from './limits.js';
|
|
4
|
+
import { isJsonObject } from './types.js';
|
|
5
|
+
export const DEFAULT_BLOB_THRESHOLD_BYTES = 64 * 1024;
|
|
6
|
+
const BLOB_KIND = 'blob';
|
|
7
|
+
const BLOB_MEDIA_TYPE = 'application/json';
|
|
8
|
+
const decodeJsonText = Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Json));
|
|
9
|
+
export function isBlobRef(value) {
|
|
10
|
+
if (!isJsonObject(value) || Object.keys(value).length !== 4) {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
const kind = value.$looms;
|
|
14
|
+
const sha256 = value.sha256;
|
|
15
|
+
const bytes = value.bytes;
|
|
16
|
+
const mediaType = value.mediaType;
|
|
17
|
+
return (kind === BLOB_KIND &&
|
|
18
|
+
Predicate.isString(sha256) &&
|
|
19
|
+
isSha256Digest(sha256) &&
|
|
20
|
+
Predicate.isNumber(bytes) &&
|
|
21
|
+
Number.isInteger(bytes) &&
|
|
22
|
+
bytes >= 0 &&
|
|
23
|
+
mediaType === BLOB_MEDIA_TYPE);
|
|
24
|
+
}
|
|
25
|
+
/** Store `value` and return a reference, even when it is under the offload threshold. */
|
|
26
|
+
export function claimJson(value, blobs) {
|
|
27
|
+
return putJson(value, blobs);
|
|
28
|
+
}
|
|
29
|
+
/** Replace values larger than `thresholdBytes` with {@link BlobRef}s. Bodies are stored by hash. */
|
|
30
|
+
export function offloadJson(value, blobs, options) {
|
|
31
|
+
const thresholdBytes = options?.thresholdBytes ?? DEFAULT_BLOB_THRESHOLD_BYTES;
|
|
32
|
+
return offloadNode(value, blobs, thresholdBytes, options?.claimRoot ?? true);
|
|
33
|
+
}
|
|
34
|
+
/** Replace {@link BlobRef}s with the JSON they name, including references nested inside a body. */
|
|
35
|
+
export function resolveJson(value, blobs) {
|
|
36
|
+
return resolveNode(value, blobs, new Set());
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Digests named by `value`, plus digests nested inside those bodies.
|
|
40
|
+
* A parent blob that itself contains references still keeps those bodies alive.
|
|
41
|
+
*/
|
|
42
|
+
export function expandBlobDigests(blobs, seeds) {
|
|
43
|
+
if (seeds.size === 0) {
|
|
44
|
+
return Effect.succeed(new Set());
|
|
45
|
+
}
|
|
46
|
+
return Effect.gen(function* () {
|
|
47
|
+
const all = new Set();
|
|
48
|
+
const pending = [...seeds];
|
|
49
|
+
while (pending.length > 0) {
|
|
50
|
+
const digest = pending.pop();
|
|
51
|
+
if (digest === undefined || all.has(digest)) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
all.add(digest);
|
|
55
|
+
const body = yield* blobs.get(digest);
|
|
56
|
+
const json = yield* decodeBody(body);
|
|
57
|
+
const nested = new Set();
|
|
58
|
+
collectBlobDigests(json, nested);
|
|
59
|
+
for (const child of nested) {
|
|
60
|
+
if (!all.has(child)) {
|
|
61
|
+
pending.push(child);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return all;
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
/** Record every blob digest embedded in `value`. Does not read blob bodies. */
|
|
69
|
+
export function collectBlobDigests(value, into) {
|
|
70
|
+
if (isBlobRef(value)) {
|
|
71
|
+
into.add(value.sha256);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (Array.isArray(value)) {
|
|
75
|
+
for (const item of value) {
|
|
76
|
+
collectBlobDigests(item, into);
|
|
77
|
+
}
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (isJsonObject(value)) {
|
|
81
|
+
for (const child of Object.values(value)) {
|
|
82
|
+
collectBlobDigests(child, into);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function offloadNode(value, blobs, thresholdBytes, claim) {
|
|
87
|
+
if (isBlobRef(value)) {
|
|
88
|
+
return Effect.succeed(value);
|
|
89
|
+
}
|
|
90
|
+
if (Array.isArray(value)) {
|
|
91
|
+
return Effect.forEach(value, (item) => offloadNode(item, blobs, thresholdBytes, true)).pipe(Effect.flatMap((items) => storeIfOver(items, blobs, thresholdBytes, claim)));
|
|
92
|
+
}
|
|
93
|
+
if (isJsonObject(value)) {
|
|
94
|
+
return offloadObject(value, blobs, thresholdBytes, claim);
|
|
95
|
+
}
|
|
96
|
+
return storeIfOver(value, blobs, thresholdBytes, claim);
|
|
97
|
+
}
|
|
98
|
+
function offloadObject(value, blobs, thresholdBytes, claim) {
|
|
99
|
+
return Effect.gen(function* () {
|
|
100
|
+
const next = {};
|
|
101
|
+
for (const key of Object.keys(value)) {
|
|
102
|
+
const child = value[key];
|
|
103
|
+
if (child === undefined) {
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
next[key] = yield* offloadNode(child, blobs, thresholdBytes, true);
|
|
107
|
+
}
|
|
108
|
+
return yield* storeIfOver(next, blobs, thresholdBytes, claim);
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
function storeIfOver(value, blobs, thresholdBytes, claim) {
|
|
112
|
+
// Strings only. Objects and arrays stay in place so schema-shaped snapshot state still decodes.
|
|
113
|
+
// An event that is still over the byte limit claims its whole payload afterward.
|
|
114
|
+
if (!claim || !Predicate.isString(value) || utf8JsonBytes(value) <= thresholdBytes) {
|
|
115
|
+
return Effect.succeed(value);
|
|
116
|
+
}
|
|
117
|
+
return putJson(value, blobs);
|
|
118
|
+
}
|
|
119
|
+
function putJson(value, blobs) {
|
|
120
|
+
const body = new TextEncoder().encode(JSON.stringify(value));
|
|
121
|
+
return sha256Hex(body).pipe(Effect.flatMap((sha256) => blobs.put(sha256, body).pipe(Effect.map(() => ({
|
|
122
|
+
$looms: BLOB_KIND,
|
|
123
|
+
sha256,
|
|
124
|
+
bytes: body.byteLength,
|
|
125
|
+
mediaType: BLOB_MEDIA_TYPE,
|
|
126
|
+
})))));
|
|
127
|
+
}
|
|
128
|
+
function resolveNode(value, blobs, seen) {
|
|
129
|
+
if (isBlobRef(value)) {
|
|
130
|
+
if (seen.has(value.sha256)) {
|
|
131
|
+
return Effect.fail(new BlobStoreError(`blob ${value.sha256} refers to itself`));
|
|
132
|
+
}
|
|
133
|
+
const next = new Set(seen);
|
|
134
|
+
next.add(value.sha256);
|
|
135
|
+
return blobs.get(value.sha256).pipe(Effect.flatMap((body) => decodeBody(body)), Effect.flatMap((decoded) => resolveNode(decoded, blobs, next)));
|
|
136
|
+
}
|
|
137
|
+
if (Array.isArray(value)) {
|
|
138
|
+
return Effect.forEach(value, (item) => resolveNode(item, blobs, seen));
|
|
139
|
+
}
|
|
140
|
+
if (isJsonObject(value)) {
|
|
141
|
+
return resolveObject(value, blobs, seen);
|
|
142
|
+
}
|
|
143
|
+
return Effect.succeed(value);
|
|
144
|
+
}
|
|
145
|
+
function resolveObject(value, blobs, seen) {
|
|
146
|
+
return Effect.gen(function* () {
|
|
147
|
+
const next = {};
|
|
148
|
+
for (const key of Object.keys(value)) {
|
|
149
|
+
const child = value[key];
|
|
150
|
+
if (child === undefined) {
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
next[key] = yield* resolveNode(child, blobs, seen);
|
|
154
|
+
}
|
|
155
|
+
return next;
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
function decodeBody(body) {
|
|
159
|
+
return decodeJsonText(new TextDecoder().decode(body)).pipe(Effect.mapError((cause) => new BlobStoreError('blob body is not JSON', cause)));
|
|
160
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Effect } from 'effect';
|
|
2
|
+
import type { VersionedDefinition } from './definition-store';
|
|
3
|
+
import type { EventEnvelope } from './envelope';
|
|
4
|
+
declare const DefinitionResolveError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
|
|
5
|
+
readonly _tag: "DefinitionResolveError";
|
|
6
|
+
} & Readonly<A>;
|
|
7
|
+
export declare class DefinitionResolveError extends DefinitionResolveError_base<{
|
|
8
|
+
readonly kind: string;
|
|
9
|
+
readonly name: string;
|
|
10
|
+
readonly version: string;
|
|
11
|
+
readonly message: string;
|
|
12
|
+
}> {
|
|
13
|
+
constructor(kind: string, name: string, version: string, message: string);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Loads one definition by the identity pinned on a run.
|
|
17
|
+
* `peek` is synchronous and only returns a definition this resolver already holds.
|
|
18
|
+
* `resolve` may fetch, and it caches through {@link cachedDefinitionResolver}.
|
|
19
|
+
*/
|
|
20
|
+
export interface DefinitionResolver {
|
|
21
|
+
readonly peek: (kind: string, name: string, version: string) => VersionedDefinition | undefined;
|
|
22
|
+
readonly resolve: (kind: string, name: string, version: string) => Effect.Effect<VersionedDefinition | undefined, DefinitionResolveError>;
|
|
23
|
+
}
|
|
24
|
+
export declare const DEFAULT_DEFINITION_CACHE_CAPACITY = 256;
|
|
25
|
+
/** Definitions registered on a module. A miss is `undefined`, not an error. */
|
|
26
|
+
export declare function staticDefinitionResolver(definitions: ReadonlyArray<VersionedDefinition>): DefinitionResolver;
|
|
27
|
+
/** First resolver that returns a definition wins. Later resolvers are not called. */
|
|
28
|
+
export declare function chainDefinitionResolvers(resolvers: ReadonlyArray<DefinitionResolver>): DefinitionResolver;
|
|
29
|
+
/**
|
|
30
|
+
* Remembers successful lookups and misses. A miss stays cached so recovery does not
|
|
31
|
+
* fetch a deleted definition on every wake. Failures are not cached.
|
|
32
|
+
*/
|
|
33
|
+
export declare function cachedDefinitionResolver(inner: DefinitionResolver, options?: {
|
|
34
|
+
readonly capacity?: number;
|
|
35
|
+
}): DefinitionResolver;
|
|
36
|
+
/** Identities pinned by `runtime.run.started` and `runtime.thread.started`. */
|
|
37
|
+
export declare function pinnedDefinitionRefs(events: readonly EventEnvelope[]): readonly VersionedDefinition[];
|
|
38
|
+
/** Resolves every pinned identity so a later synchronous lookup can see it. */
|
|
39
|
+
export declare function primePinnedDefinitions(resolver: DefinitionResolver, events: readonly EventEnvelope[]): Effect.Effect<void, DefinitionResolveError>;
|
|
40
|
+
export {};
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { Data, Effect, Predicate } from 'effect';
|
|
2
|
+
import { DEFAULT_DEFINITION_VERSION, definitionKey } from './module.js';
|
|
3
|
+
export class DefinitionResolveError extends Data.TaggedError('DefinitionResolveError') {
|
|
4
|
+
constructor(kind, name, version, message) {
|
|
5
|
+
super({ kind, name, version, message });
|
|
6
|
+
this.name = 'DefinitionResolveError';
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export const DEFAULT_DEFINITION_CACHE_CAPACITY = 256;
|
|
10
|
+
/** Definitions registered on a module. A miss is `undefined`, not an error. */
|
|
11
|
+
export function staticDefinitionResolver(definitions) {
|
|
12
|
+
const byKey = new Map(definitions.map((definition) => [definitionKey(definition.kind, definition.name, definition.version), definition]));
|
|
13
|
+
const lookup = (kind, name, version) => byKey.get(definitionKey(kind, name, version));
|
|
14
|
+
return {
|
|
15
|
+
peek: lookup,
|
|
16
|
+
resolve: (kind, name, version) => Effect.succeed(lookup(kind, name, version)),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/** First resolver that returns a definition wins. Later resolvers are not called. */
|
|
20
|
+
export function chainDefinitionResolvers(resolvers) {
|
|
21
|
+
return {
|
|
22
|
+
peek(kind, name, version) {
|
|
23
|
+
for (const resolver of resolvers) {
|
|
24
|
+
const found = resolver.peek(kind, name, version);
|
|
25
|
+
if (found) {
|
|
26
|
+
return found;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return undefined;
|
|
30
|
+
},
|
|
31
|
+
resolve(kind, name, version) {
|
|
32
|
+
return Effect.gen(function* () {
|
|
33
|
+
for (const resolver of resolvers) {
|
|
34
|
+
const found = yield* resolver.resolve(kind, name, version);
|
|
35
|
+
if (found) {
|
|
36
|
+
return found;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return undefined;
|
|
40
|
+
});
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Remembers successful lookups and misses. A miss stays cached so recovery does not
|
|
46
|
+
* fetch a deleted definition on every wake. Failures are not cached.
|
|
47
|
+
*/
|
|
48
|
+
export function cachedDefinitionResolver(inner, options) {
|
|
49
|
+
const capacity = options?.capacity ?? DEFAULT_DEFINITION_CACHE_CAPACITY;
|
|
50
|
+
const entries = new Map();
|
|
51
|
+
const remember = (key, value) => {
|
|
52
|
+
entries.delete(key);
|
|
53
|
+
entries.set(key, value);
|
|
54
|
+
while (entries.size > capacity) {
|
|
55
|
+
const oldest = entries.keys().next().value;
|
|
56
|
+
if (oldest === undefined) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
entries.delete(oldest);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
return {
|
|
63
|
+
peek(kind, name, version) {
|
|
64
|
+
const key = definitionKey(kind, name, version);
|
|
65
|
+
if (!entries.has(key)) {
|
|
66
|
+
return inner.peek(kind, name, version);
|
|
67
|
+
}
|
|
68
|
+
const value = entries.get(key);
|
|
69
|
+
remember(key, value);
|
|
70
|
+
return value;
|
|
71
|
+
},
|
|
72
|
+
resolve(kind, name, version) {
|
|
73
|
+
const key = definitionKey(kind, name, version);
|
|
74
|
+
if (entries.has(key)) {
|
|
75
|
+
const value = entries.get(key);
|
|
76
|
+
remember(key, value);
|
|
77
|
+
return Effect.succeed(value);
|
|
78
|
+
}
|
|
79
|
+
return inner.resolve(kind, name, version).pipe(Effect.tap((value) => Effect.sync(() => {
|
|
80
|
+
remember(key, value);
|
|
81
|
+
})));
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/** Identities pinned by `runtime.run.started` and `runtime.thread.started`. */
|
|
86
|
+
export function pinnedDefinitionRefs(events) {
|
|
87
|
+
const refs = [];
|
|
88
|
+
const seen = new Set();
|
|
89
|
+
for (const event of events) {
|
|
90
|
+
if (event.type !== 'runtime.run.started' && event.type !== 'runtime.thread.started') {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
const pinned = readPinned(event.payload);
|
|
94
|
+
if (!pinned) {
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
const key = definitionKey(pinned.kind, pinned.name, pinned.version);
|
|
98
|
+
if (seen.has(key)) {
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
seen.add(key);
|
|
102
|
+
refs.push(pinned);
|
|
103
|
+
}
|
|
104
|
+
return refs;
|
|
105
|
+
}
|
|
106
|
+
/** Resolves every pinned identity so a later synchronous lookup can see it. */
|
|
107
|
+
export function primePinnedDefinitions(resolver, events) {
|
|
108
|
+
return Effect.gen(function* () {
|
|
109
|
+
for (const ref of pinnedDefinitionRefs(events)) {
|
|
110
|
+
yield* resolver.resolve(ref.kind, ref.name, ref.version);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
function readPinned(payload) {
|
|
115
|
+
if (!Predicate.isObject(payload) || !('kind' in payload) || !('definitionName' in payload)) {
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
const kind = payload.kind;
|
|
119
|
+
const name = payload.definitionName;
|
|
120
|
+
const version = 'definitionVersion' in payload ? payload.definitionVersion : undefined;
|
|
121
|
+
if (!Predicate.isString(kind) || !Predicate.isString(name)) {
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
kind,
|
|
126
|
+
name,
|
|
127
|
+
version: Predicate.isString(version) ? version : DEFAULT_DEFINITION_VERSION,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { Effect } from 'effect';
|
|
2
|
+
import type { DefinitionResolveError, DefinitionResolver } from './definition-resolver';
|
|
1
3
|
export interface VersionedDefinition {
|
|
2
4
|
readonly kind: string;
|
|
3
5
|
readonly name: string;
|
|
@@ -6,6 +8,10 @@ export interface VersionedDefinition {
|
|
|
6
8
|
/** Lookup of one definition kind by name and version. */
|
|
7
9
|
export interface DefinitionStore<TDefinition> {
|
|
8
10
|
get(name: string, version: string): TDefinition | undefined;
|
|
11
|
+
/** Static definition, or a definition the bound resolver has already loaded. */
|
|
12
|
+
resolve(name: string, version: string): Effect.Effect<TDefinition | undefined, DefinitionResolveError>;
|
|
13
|
+
/** Called by the runtime before the module's services are built. */
|
|
14
|
+
useResolver(resolver: DefinitionResolver): void;
|
|
9
15
|
}
|
|
10
16
|
/** Indexes definitions of one kind by `definitionKey`. */
|
|
11
17
|
export declare function makeDefinitionStore<TDefinition extends VersionedDefinition>(kind: TDefinition['kind'], definitions: ReadonlyArray<TDefinition>): DefinitionStore<TDefinition>;
|
|
@@ -1,6 +1,42 @@
|
|
|
1
|
+
import { Effect } from 'effect';
|
|
1
2
|
import { definitionKey } from './module.js';
|
|
2
3
|
/** Indexes definitions of one kind by `definitionKey`. */
|
|
3
4
|
export function makeDefinitionStore(kind, definitions) {
|
|
4
5
|
const byKey = new Map(definitions.map((definition) => [definitionKey(definition.kind, definition.name, definition.version), definition]));
|
|
5
|
-
|
|
6
|
+
let resolver;
|
|
7
|
+
const get = (name, version) => {
|
|
8
|
+
const stored = byKey.get(definitionKey(kind, name, version));
|
|
9
|
+
if (stored) {
|
|
10
|
+
return stored;
|
|
11
|
+
}
|
|
12
|
+
const peeked = resolver?.peek(kind, name, version);
|
|
13
|
+
if (peeked && isStoredDefinition(kind, peeked)) {
|
|
14
|
+
return peeked;
|
|
15
|
+
}
|
|
16
|
+
return undefined;
|
|
17
|
+
};
|
|
18
|
+
return {
|
|
19
|
+
get,
|
|
20
|
+
useResolver(next) {
|
|
21
|
+
resolver = next;
|
|
22
|
+
},
|
|
23
|
+
resolve(name, version) {
|
|
24
|
+
const stored = get(name, version);
|
|
25
|
+
if (stored) {
|
|
26
|
+
return Effect.succeed(stored);
|
|
27
|
+
}
|
|
28
|
+
if (!resolver) {
|
|
29
|
+
// Effect.void is Effect<void>, which is not a missing definition.
|
|
30
|
+
// oxlint-disable-next-line effecttsgo/effect-succeed-with-void
|
|
31
|
+
return Effect.succeed(undefined);
|
|
32
|
+
}
|
|
33
|
+
return resolver
|
|
34
|
+
.resolve(kind, name, version)
|
|
35
|
+
.pipe(Effect.map((found) => found && isStoredDefinition(kind, found) ? found : undefined));
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
// oxlint-disable-next-line typescript/no-unnecessary-type-parameters -- the predicate returns the store's definition type
|
|
40
|
+
function isStoredDefinition(kind, value) {
|
|
41
|
+
return value.kind === kind;
|
|
6
42
|
}
|