@cosmicdrift/kumiko-framework 0.2.2 → 0.3.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/CHANGELOG.md +54 -0
- package/package.json +124 -38
- package/src/__tests__/full-stack.integration.ts +2 -2
- package/src/api/auth-routes.ts +5 -5
- package/src/api/jwt.ts +2 -2
- package/src/api/route-registrars.ts +1 -1
- package/src/api/routes.ts +3 -3
- package/src/api/server.ts +6 -7
- package/src/auth/__tests__/roles.test.ts +24 -0
- package/src/auth/index.ts +7 -0
- package/src/auth/roles.ts +42 -0
- package/src/compliance/__tests__/duration-spec.test.ts +72 -0
- package/src/compliance/__tests__/profiles.test.ts +308 -0
- package/src/compliance/__tests__/sub-processors.test.ts +139 -0
- package/src/compliance/duration-spec.ts +44 -0
- package/src/compliance/index.ts +31 -0
- package/src/compliance/override-schema.ts +136 -0
- package/src/compliance/profiles.ts +427 -0
- package/src/compliance/sub-processors.ts +152 -0
- package/src/db/__tests__/big-int-field.test.ts +131 -0
- package/src/db/assert-exists-in.ts +2 -2
- package/src/db/cursor.ts +3 -3
- package/src/db/event-store-executor.ts +19 -13
- package/src/db/located-timestamp.ts +1 -1
- package/src/db/money.ts +12 -2
- package/src/db/pg-error.ts +1 -1
- package/src/db/row-helpers.ts +1 -1
- package/src/db/table-builder.ts +20 -5
- package/src/db/tenant-db.ts +9 -9
- package/src/engine/__tests__/_pipeline-test-utils.ts +23 -0
- package/src/engine/__tests__/boot-validator-api-exposure.test.ts +142 -0
- package/src/engine/__tests__/boot-validator-pii-retention.test.ts +570 -0
- package/src/engine/__tests__/boot-validator-s0-integration.test.ts +160 -0
- package/src/engine/__tests__/build-target.test.ts +135 -0
- package/src/engine/__tests__/codemod-pipeline.test.ts +551 -0
- package/src/engine/__tests__/entity-handlers.test.ts +3 -3
- package/src/engine/__tests__/event-helpers.test.ts +4 -4
- package/src/engine/__tests__/pipeline-engine.test.ts +215 -0
- package/src/engine/__tests__/pipeline-handler.integration.ts +894 -0
- package/src/engine/__tests__/pipeline-observability.integration.ts +142 -0
- package/src/engine/__tests__/pipeline-performance.integration.ts +152 -0
- package/src/engine/__tests__/pipeline-sub-pipelines.test.ts +288 -0
- package/src/engine/__tests__/raw-table.test.ts +2 -2
- package/src/engine/__tests__/steps-aggregate-append-event.test.ts +115 -0
- package/src/engine/__tests__/steps-aggregate-create.test.ts +92 -0
- package/src/engine/__tests__/steps-aggregate-update.test.ts +127 -0
- package/src/engine/__tests__/steps-call-feature.test.ts +123 -0
- package/src/engine/__tests__/steps-mail-send.test.ts +136 -0
- package/src/engine/__tests__/steps-read.test.ts +142 -0
- package/src/engine/__tests__/steps-resolver-utils.test.ts +50 -0
- package/src/engine/__tests__/steps-unsafe-projection-delete.test.ts +69 -0
- package/src/engine/__tests__/steps-unsafe-projection-upsert.test.ts +117 -0
- package/src/engine/__tests__/steps-webhook-send.test.ts +135 -0
- package/src/engine/__tests__/steps-workflow.test.ts +198 -0
- package/src/engine/__tests__/validate-projection-allowlist.test.ts +491 -0
- package/src/engine/__tests__/visual-tree-patterns.test.ts +251 -0
- package/src/engine/boot-validator/api-ext.ts +77 -0
- package/src/engine/boot-validator/config-deps.ts +163 -0
- package/src/engine/boot-validator/entity-handler.ts +466 -0
- package/src/engine/boot-validator/index.ts +159 -0
- package/src/engine/boot-validator/ownership.ts +198 -0
- package/src/engine/boot-validator/pii-retention.ts +155 -0
- package/src/engine/boot-validator/screens-nav.ts +624 -0
- package/src/engine/boot-validator.ts +1 -1528
- package/src/engine/build-app-schema.ts +1 -1
- package/src/engine/build-target.ts +99 -0
- package/src/engine/codemod/index.ts +15 -0
- package/src/engine/codemod/pipeline-codemod.ts +641 -0
- package/src/engine/config-helpers.ts +9 -19
- package/src/engine/constants.ts +1 -1
- package/src/engine/define-feature.ts +127 -9
- package/src/engine/define-handler.ts +89 -3
- package/src/engine/define-roles.ts +2 -2
- package/src/engine/define-step.ts +28 -0
- package/src/engine/define-workflow.ts +110 -0
- package/src/engine/entity-handlers.ts +10 -9
- package/src/engine/event-helpers.ts +4 -4
- package/src/engine/extension-names.ts +105 -0
- package/src/engine/extensions/user-data.ts +106 -0
- package/src/engine/factories.ts +26 -16
- package/src/engine/feature-ast/__tests__/visual-tree-parse.test.ts +184 -0
- package/src/engine/feature-ast/extractors/index.ts +74 -0
- package/src/engine/feature-ast/extractors/round1.ts +110 -0
- package/src/engine/feature-ast/extractors/round2.ts +253 -0
- package/src/engine/feature-ast/extractors/round3.ts +471 -0
- package/src/engine/feature-ast/extractors/round4.ts +1365 -0
- package/src/engine/feature-ast/extractors/round5.ts +72 -0
- package/src/engine/feature-ast/extractors/round6.ts +66 -0
- package/src/engine/feature-ast/extractors/shared.ts +177 -0
- package/src/engine/feature-ast/parse.ts +13 -0
- package/src/engine/feature-ast/patch.ts +9 -1
- package/src/engine/feature-ast/patcher.ts +10 -3
- package/src/engine/feature-ast/patterns.ts +71 -1
- package/src/engine/feature-ast/render.ts +31 -1
- package/src/engine/index.ts +66 -2
- package/src/engine/pattern-library/__tests__/library.test.ts +11 -0
- package/src/engine/pattern-library/library.ts +78 -2
- package/src/engine/pipeline.ts +88 -0
- package/src/engine/projection-helpers.ts +1 -1
- package/src/engine/read-claim.ts +1 -1
- package/src/engine/registry.ts +30 -2
- package/src/engine/resolve-config-or-param.ts +4 -0
- package/src/engine/run-pipeline.ts +162 -0
- package/src/engine/schema-builder.ts +10 -4
- package/src/engine/state-machine.ts +1 -1
- package/src/engine/steps/_drizzle-boundary.ts +19 -0
- package/src/engine/steps/_duration-utils.ts +33 -0
- package/src/engine/steps/_no-return-guard.ts +21 -0
- package/src/engine/steps/_resolver-utils.ts +42 -0
- package/src/engine/steps/_step-dispatch-constants.ts +38 -0
- package/src/engine/steps/aggregate-append-event.ts +56 -0
- package/src/engine/steps/aggregate-create.ts +56 -0
- package/src/engine/steps/aggregate-update.ts +68 -0
- package/src/engine/steps/branch.ts +84 -0
- package/src/engine/steps/call-feature.ts +49 -0
- package/src/engine/steps/compute.ts +41 -0
- package/src/engine/steps/for-each.ts +111 -0
- package/src/engine/steps/mail-send.ts +44 -0
- package/src/engine/steps/read-find-many.ts +51 -0
- package/src/engine/steps/read-find-one.ts +58 -0
- package/src/engine/steps/retry.ts +87 -0
- package/src/engine/steps/return.ts +34 -0
- package/src/engine/steps/unsafe-projection-delete.ts +46 -0
- package/src/engine/steps/unsafe-projection-upsert.ts +69 -0
- package/src/engine/steps/wait-for-event.ts +71 -0
- package/src/engine/steps/wait.ts +69 -0
- package/src/engine/steps/webhook-send.ts +71 -0
- package/src/engine/system-user.ts +1 -1
- package/src/engine/types/feature.ts +143 -1
- package/src/engine/types/fields.ts +134 -10
- package/src/engine/types/handlers.ts +18 -10
- package/src/engine/types/identifiers.ts +1 -0
- package/src/engine/types/index.ts +15 -1
- package/src/engine/types/step.ts +334 -0
- package/src/engine/types/target-ref.ts +21 -0
- package/src/engine/types/tree-node.ts +130 -0
- package/src/engine/types/workspace.ts +7 -0
- package/src/engine/validate-projection-allowlist.ts +161 -0
- package/src/event-store/snapshot.ts +1 -1
- package/src/event-store/upcaster-dead-letter.ts +1 -1
- package/src/event-store/upcaster.ts +1 -1
- package/src/files/__tests__/read-stream.test.ts +105 -0
- package/src/files/__tests__/write-stream.test.ts +233 -0
- package/src/files/__tests__/zip-stream.test.ts +357 -0
- package/src/files/file-routes.ts +1 -1
- package/src/files/in-memory-provider.ts +38 -0
- package/src/files/index.ts +3 -0
- package/src/files/local-provider.ts +58 -1
- package/src/files/types.ts +36 -8
- package/src/files/zip-stream.ts +251 -0
- package/src/jobs/job-runner.ts +10 -10
- package/src/lifecycle/lifecycle.ts +0 -3
- package/src/logging/index.ts +1 -0
- package/src/logging/pino-logger.ts +11 -7
- package/src/logging/utils.ts +24 -0
- package/src/observability/prometheus-meter.ts +7 -5
- package/src/pipeline/__tests__/archive-stream.integration.ts +1 -1
- package/src/pipeline/__tests__/causation-chain.integration.ts +1 -1
- package/src/pipeline/__tests__/domain-events-projections.integration.ts +3 -3
- package/src/pipeline/__tests__/event-define-event-strict.integration.ts +4 -4
- package/src/pipeline/__tests__/load-aggregate-query.integration.ts +1 -1
- package/src/pipeline/__tests__/msp-multi-hop.integration.ts +3 -3
- package/src/pipeline/__tests__/msp-rebuild.integration.ts +3 -3
- package/src/pipeline/__tests__/multi-stream-projection.integration.ts +2 -2
- package/src/pipeline/__tests__/query-projection.integration.ts +5 -5
- package/src/pipeline/append-event-core.ts +22 -6
- package/src/pipeline/dispatcher-utils.ts +188 -0
- package/src/pipeline/dispatcher.ts +63 -283
- package/src/pipeline/distributed-lock.ts +1 -1
- package/src/pipeline/entity-cache.ts +2 -2
- package/src/pipeline/event-consumer-state.ts +0 -13
- package/src/pipeline/event-dispatcher.ts +4 -4
- package/src/pipeline/index.ts +0 -2
- package/src/pipeline/lifecycle-pipeline.ts +6 -12
- package/src/pipeline/msp-rebuild.ts +5 -5
- package/src/pipeline/multi-stream-apply-context.ts +6 -7
- package/src/pipeline/projection-rebuild.ts +2 -2
- package/src/pipeline/projection-state.ts +0 -12
- package/src/rate-limit/__tests__/resolver.integration.ts +8 -4
- package/src/rate-limit/resolver.ts +1 -1
- package/src/search/in-memory-adapter.ts +1 -1
- package/src/search/meilisearch-adapter.ts +3 -3
- package/src/search/types.ts +1 -1
- package/src/secrets/leak-guard.ts +2 -2
- package/src/stack/request-helper.ts +9 -5
- package/src/stack/test-stack.ts +1 -1
- package/src/testing/handler-context.ts +4 -4
- package/src/testing/http-cookies.ts +1 -1
- package/src/time/tz-context.ts +1 -2
- package/src/ui-types/index.ts +4 -0
- package/src/engine/feature-ast/extractors.ts +0 -2562
|
@@ -115,5 +115,5 @@ export async function listDeadLetters(
|
|
|
115
115
|
.from(upcasterDeadLetterTable)
|
|
116
116
|
.orderBy(desc(upcasterDeadLetterTable.createdAt))
|
|
117
117
|
.limit(limit);
|
|
118
|
-
return rows as readonly DeadLetterRow[];
|
|
118
|
+
return rows as readonly DeadLetterRow[]; // @cast-boundary db-row
|
|
119
119
|
}
|
|
@@ -88,7 +88,7 @@ async function upcastStoredEventWithPolicy(
|
|
|
88
88
|
if (!info) return event;
|
|
89
89
|
if (event.eventVersion >= info.currentVersion) return event;
|
|
90
90
|
|
|
91
|
-
let payload = event.payload as unknown;
|
|
91
|
+
let payload = event.payload as unknown; // @cast-boundary engine-payload
|
|
92
92
|
let v = event.eventVersion;
|
|
93
93
|
const startVersion = event.eventVersion;
|
|
94
94
|
while (v < info.currentVersion) {
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// Storage-Provider readStream-API (S2.U3 Atom 3c.fix).
|
|
2
|
+
//
|
|
3
|
+
// Pinst:
|
|
4
|
+
// - In-memory-Provider: readStream yieldet die Bytes als single-chunk,
|
|
5
|
+
// Roundtrip identisch zu write+read.
|
|
6
|
+
// - Local-Provider: readStream nutzt fs.createReadStream → mehrere
|
|
7
|
+
// chunks fuer >hwm-Files. Lazy-Fail: ENOENT trifft erst beim ersten
|
|
8
|
+
// chunk-pull, nicht beim readStream-Aufruf.
|
|
9
|
+
// - Beide Provider: missing-key throw't beim Konsum, nicht bei
|
|
10
|
+
// readStream() — gleiches Lazy-Pattern wie S3.
|
|
11
|
+
//
|
|
12
|
+
// readStream + writeStream sind ab Atom 3c.fix REQUIRED in der Provider-
|
|
13
|
+
// Surface (kein optional). Der Type-Compiler erzwingt Implementierung,
|
|
14
|
+
// kein silent runtime-throw mehr.
|
|
15
|
+
|
|
16
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
17
|
+
import { tmpdir } from "node:os";
|
|
18
|
+
import { join } from "node:path";
|
|
19
|
+
import { afterEach, beforeEach, describe, expect, test } from "vitest";
|
|
20
|
+
import { createInMemoryFileProvider } from "../in-memory-provider";
|
|
21
|
+
import { createLocalProvider } from "../local-provider";
|
|
22
|
+
|
|
23
|
+
async function collect(stream: AsyncIterable<Uint8Array>): Promise<Uint8Array> {
|
|
24
|
+
const chunks: Uint8Array[] = [];
|
|
25
|
+
let total = 0;
|
|
26
|
+
for await (const chunk of stream) {
|
|
27
|
+
chunks.push(chunk);
|
|
28
|
+
total += chunk.byteLength;
|
|
29
|
+
}
|
|
30
|
+
const out = new Uint8Array(total);
|
|
31
|
+
let offset = 0;
|
|
32
|
+
for (const c of chunks) {
|
|
33
|
+
out.set(c, offset);
|
|
34
|
+
offset += c.byteLength;
|
|
35
|
+
}
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe("FileStorageProvider.readStream — in-memory", () => {
|
|
40
|
+
test("readStream returnt geschriebene Bytes identisch", async () => {
|
|
41
|
+
const provider = createInMemoryFileProvider();
|
|
42
|
+
const data = new Uint8Array([1, 2, 3, 4, 5]);
|
|
43
|
+
await provider.write("test/foo.bin", data);
|
|
44
|
+
|
|
45
|
+
const result = await collect(provider.readStream("test/foo.bin"));
|
|
46
|
+
expect(Array.from(result)).toEqual([1, 2, 3, 4, 5]);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("missing-key throw't beim ersten chunk-pull (lazy-Pattern)", async () => {
|
|
50
|
+
const provider = createInMemoryFileProvider();
|
|
51
|
+
// readStream() selbst wirft NICHT — das Iterator-Object existiert.
|
|
52
|
+
const stream = provider.readStream("does/not/exist.bin");
|
|
53
|
+
expect(stream).toBeDefined();
|
|
54
|
+
// Erst beim Iterieren faellt der Fehler.
|
|
55
|
+
await expect(collect(stream)).rejects.toThrow(/in-memory file not found/);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe("FileStorageProvider.readStream — local", () => {
|
|
60
|
+
let basePath: string;
|
|
61
|
+
|
|
62
|
+
beforeEach(async () => {
|
|
63
|
+
basePath = await mkdtemp(join(tmpdir(), "kumiko-readstream-test-"));
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
afterEach(async () => {
|
|
67
|
+
await rm(basePath, { recursive: true, force: true });
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("readStream returnt geschriebene Bytes identisch", async () => {
|
|
71
|
+
const provider = createLocalProvider(basePath);
|
|
72
|
+
const data = new Uint8Array([10, 20, 30, 40, 50]);
|
|
73
|
+
await provider.write("foo.bin", data);
|
|
74
|
+
|
|
75
|
+
const result = await collect(provider.readStream("foo.bin"));
|
|
76
|
+
expect(Array.from(result)).toEqual([10, 20, 30, 40, 50]);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("readStream chunkt grosse Files (>64KB highWaterMark)", async () => {
|
|
80
|
+
// 200KB > default highWaterMark=64KB → erwartet mind. 2 chunks.
|
|
81
|
+
const provider = createLocalProvider(basePath);
|
|
82
|
+
const big = new Uint8Array(200 * 1024);
|
|
83
|
+
for (let i = 0; i < big.length; i++) big[i] = i & 0xff;
|
|
84
|
+
await provider.write("big.bin", big);
|
|
85
|
+
|
|
86
|
+
let chunkCount = 0;
|
|
87
|
+
let totalBytes = 0;
|
|
88
|
+
for await (const chunk of provider.readStream("big.bin")) {
|
|
89
|
+
chunkCount++;
|
|
90
|
+
totalBytes += chunk.byteLength;
|
|
91
|
+
}
|
|
92
|
+
expect(chunkCount).toBeGreaterThan(1);
|
|
93
|
+
expect(totalBytes).toBe(200 * 1024);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("ENOENT throw't beim ersten chunk-pull (lazy-Pattern)", async () => {
|
|
97
|
+
// Pinst dass readStream() selbst kein Filesystem-Lookup macht;
|
|
98
|
+
// node:fs createReadStream emittiert error event erst beim Read-
|
|
99
|
+
// Versuch. Test-Coverage matched die Inmemory-Variante.
|
|
100
|
+
const provider = createLocalProvider(basePath);
|
|
101
|
+
const stream = provider.readStream("does/not/exist.bin");
|
|
102
|
+
expect(stream).toBeDefined();
|
|
103
|
+
await expect(collect(stream)).rejects.toThrow(/ENOENT|no such file/);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
// Storage-Provider writeStream-API (S2.U3 Atom 1d).
|
|
2
|
+
//
|
|
3
|
+
// Pinst:
|
|
4
|
+
// - In-memory-Provider: writeStream collected chunks + macht
|
|
5
|
+
// read-Roundtrip identisch zu write(uint8array).
|
|
6
|
+
// - Local-Provider: writeStream schreibt atomar via tmp + rename,
|
|
7
|
+
// halb-fertige Stream-Bricht hinterlassen keine Garbage am Final-
|
|
8
|
+
// Pfad.
|
|
9
|
+
// - Beide Provider liefern die Bytes via read() identisch zurueck —
|
|
10
|
+
// kein chunk-Loss, kein chunk-Order-Verlust.
|
|
11
|
+
//
|
|
12
|
+
// AsyncIterable-source pinst die Streaming-Semantik (Caller streamt
|
|
13
|
+
// chunk-fuer-chunk, Provider niemals alles im Memory).
|
|
14
|
+
|
|
15
|
+
import { mkdtemp, readdir, rm, stat } from "node:fs/promises";
|
|
16
|
+
import { tmpdir } from "node:os";
|
|
17
|
+
import { join } from "node:path";
|
|
18
|
+
import { afterEach, beforeEach, describe, expect, test } from "vitest";
|
|
19
|
+
import { createInMemoryFileProvider } from "../in-memory-provider";
|
|
20
|
+
import { createLocalProvider } from "../local-provider";
|
|
21
|
+
|
|
22
|
+
async function* fromChunks(chunks: Uint8Array[]): AsyncIterable<Uint8Array> {
|
|
23
|
+
for (const c of chunks) {
|
|
24
|
+
yield c;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function* slowChunks(chunks: Uint8Array[], delayMs: number): AsyncIterable<Uint8Array> {
|
|
29
|
+
for (const c of chunks) {
|
|
30
|
+
await new Promise((r) => setTimeout(r, delayMs));
|
|
31
|
+
yield c;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe("FileStorageProvider.writeStream — in-memory", () => {
|
|
36
|
+
test("schreibt 3 chunks + read liefert konkateniert zurueck", async () => {
|
|
37
|
+
const provider = createInMemoryFileProvider();
|
|
38
|
+
const chunks = [
|
|
39
|
+
new Uint8Array([1, 2, 3]),
|
|
40
|
+
new Uint8Array([4, 5, 6]),
|
|
41
|
+
new Uint8Array([7, 8, 9]),
|
|
42
|
+
];
|
|
43
|
+
await provider.writeStream("test/file.bin", fromChunks(chunks));
|
|
44
|
+
|
|
45
|
+
const data = await provider.read("test/file.bin");
|
|
46
|
+
expect(Array.from(data)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9]);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("leerer Stream → leere Datei", async () => {
|
|
50
|
+
const provider = createInMemoryFileProvider();
|
|
51
|
+
await provider.writeStream("empty.bin", fromChunks([]));
|
|
52
|
+
|
|
53
|
+
const data = await provider.read("empty.bin");
|
|
54
|
+
expect(data.byteLength).toBe(0);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("mimeType-Option wird in der gespeicherten Entry erhalten", async () => {
|
|
58
|
+
// Kein direkter Read-Pfad fuer mimeType — wir checken via existing-
|
|
59
|
+
// Marker (write-Pfad teilt den storage-shape). Symmetrie-Test:
|
|
60
|
+
// ein write() + ein writeStream() mit gleichem key + gleichem
|
|
61
|
+
// mimeType muessen byte-identische Reads liefern.
|
|
62
|
+
const a = createInMemoryFileProvider();
|
|
63
|
+
const b = createInMemoryFileProvider();
|
|
64
|
+
const bytes = new Uint8Array([42, 43, 44]);
|
|
65
|
+
|
|
66
|
+
await a.write("k", bytes, "application/zip");
|
|
67
|
+
if (!b.writeStream) throw new Error("writeStream missing");
|
|
68
|
+
await b.writeStream("k", fromChunks([bytes]), { mimeType: "application/zip" });
|
|
69
|
+
|
|
70
|
+
expect(Array.from(await a.read("k"))).toEqual(Array.from(await b.read("k")));
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe("FileStorageProvider.writeStream — local-filesystem", () => {
|
|
75
|
+
let basePath: string;
|
|
76
|
+
|
|
77
|
+
beforeEach(async () => {
|
|
78
|
+
basePath = await mkdtemp(join(tmpdir(), "kumiko-write-stream-"));
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
afterEach(async () => {
|
|
82
|
+
await rm(basePath, { recursive: true, force: true });
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("schreibt + read roundtrip mit chunked source", async () => {
|
|
86
|
+
const provider = createLocalProvider(basePath);
|
|
87
|
+
const chunks = [new Uint8Array([10, 20, 30]), new Uint8Array([40, 50, 60])];
|
|
88
|
+
await provider.writeStream("dir/foo.bin", fromChunks(chunks));
|
|
89
|
+
|
|
90
|
+
const data = await provider.read("dir/foo.bin");
|
|
91
|
+
expect(Array.from(data)).toEqual([10, 20, 30, 40, 50, 60]);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("legt parent-Verzeichnisse rekursiv an", async () => {
|
|
95
|
+
const provider = createLocalProvider(basePath);
|
|
96
|
+
await provider.writeStream("deeply/nested/path/file.bin", fromChunks([new Uint8Array([1])]));
|
|
97
|
+
|
|
98
|
+
const stats = await stat(join(basePath, "deeply/nested/path/file.bin"));
|
|
99
|
+
expect(stats.isFile()).toBe(true);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("atomar: bei wirfendem Stream entsteht KEIN final-Pfad-File", async () => {
|
|
103
|
+
const provider = createLocalProvider(basePath);
|
|
104
|
+
|
|
105
|
+
async function* failingSource(): AsyncIterable<Uint8Array> {
|
|
106
|
+
yield new Uint8Array([1, 2, 3]);
|
|
107
|
+
throw new Error("synthetic mid-stream failure");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
await expect(provider.writeStream("dir/half.bin", failingSource())).rejects.toThrow(
|
|
111
|
+
/synthetic mid-stream failure/,
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
expect(await provider.exists("dir/half.bin")).toBe(false);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("atomar: nach Failure ist KEIN final-Pfad-File da (.tmp-Leak ist best-effort)", async () => {
|
|
118
|
+
// Echte Atomicity-Garantie: der final-Pfad ist niemals halb-fertig
|
|
119
|
+
// sichtbar. tmp-Files koennen je nach OS-Race im destroy-Pfad
|
|
120
|
+
// kurz liegen bleiben — kein Korrektheitsproblem (kein Reader
|
|
121
|
+
// sucht nach `*.tmp`-Patterns), nur Operations-Hygiene.
|
|
122
|
+
const provider = createLocalProvider(basePath);
|
|
123
|
+
|
|
124
|
+
async function* failing(): AsyncIterable<Uint8Array> {
|
|
125
|
+
yield new Uint8Array([99]);
|
|
126
|
+
throw new Error("fail");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
await expect(provider.writeStream("subdir/leak-check.bin", failing())).rejects.toThrow();
|
|
130
|
+
|
|
131
|
+
// Final-Pfad ist NICHT da — das ist die harte Garantie.
|
|
132
|
+
expect(await provider.exists("subdir/leak-check.bin")).toBe(false);
|
|
133
|
+
|
|
134
|
+
// tmp-Files duerfen nicht den final-Namen haben (sonst kein Atomicity).
|
|
135
|
+
const entries = await readdir(join(basePath, "subdir"));
|
|
136
|
+
for (const entry of entries) {
|
|
137
|
+
expect(entry).not.toBe("leak-check.bin");
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("ueberschreibt existing file atomar (rename ueber bestehenden Pfad)", async () => {
|
|
142
|
+
const provider = createLocalProvider(basePath);
|
|
143
|
+
|
|
144
|
+
await provider.write("k", new Uint8Array([1, 1, 1]));
|
|
145
|
+
await provider.writeStream("k", fromChunks([new Uint8Array([2, 2, 2])]));
|
|
146
|
+
|
|
147
|
+
const data = await provider.read("k");
|
|
148
|
+
expect(Array.from(data)).toEqual([2, 2, 2]);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
describe("FileStorageProvider.writeStream — Streaming-Property", () => {
|
|
153
|
+
// Pinst dass writeStream tatsaechlich AsyncIterable konsumiert ohne
|
|
154
|
+
// alle chunks in eine Promise.resolve(...) Array zu converten.
|
|
155
|
+
// Wenn ein Provider intern das chunks-Array sammelt, ist das fuer
|
|
156
|
+
// unsere semantischen Garantien OK — wir testen nur das Resultat.
|
|
157
|
+
// Dieser Test sichert dass der Caller ein REIN async-iterable
|
|
158
|
+
// uebergeben kann (z.B. ZIP-Stream der chunks lazy generiert).
|
|
159
|
+
|
|
160
|
+
test("Source mit Promise-delays wird korrekt verarbeitet", async () => {
|
|
161
|
+
const provider = createInMemoryFileProvider();
|
|
162
|
+
|
|
163
|
+
async function* lazySource(): AsyncIterable<Uint8Array> {
|
|
164
|
+
await new Promise((r) => setTimeout(r, 5));
|
|
165
|
+
yield new Uint8Array([1]);
|
|
166
|
+
await new Promise((r) => setTimeout(r, 5));
|
|
167
|
+
yield new Uint8Array([2]);
|
|
168
|
+
await new Promise((r) => setTimeout(r, 5));
|
|
169
|
+
yield new Uint8Array([3]);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
await provider.writeStream("lazy.bin", lazySource());
|
|
173
|
+
|
|
174
|
+
const data = await provider.read("lazy.bin");
|
|
175
|
+
expect(Array.from(data)).toEqual([1, 2, 3]);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("local-Provider streamt WAEHREND der Source yieldet (tmp-File existiert pre-completion)", async () => {
|
|
179
|
+
// Echter Streaming-Property-Test: bei langsamen Source-yields muss
|
|
180
|
+
// der local-Provider die tmp-File anfangen zu schreiben WAEHREND
|
|
181
|
+
// wir noch chunks zur Verfuegung stellen — nicht erst alle chunks
|
|
182
|
+
// collecten.
|
|
183
|
+
//
|
|
184
|
+
// Pattern: 5 chunks mit je 30ms delay zwischen yields. Wir starten
|
|
185
|
+
// writeStream + pollen alle 10ms (max 200ms) bis tmp-File auftaucht.
|
|
186
|
+
// Bei collect-then-write taucht NIE eine tmp-File auf — Test failed
|
|
187
|
+
// dann via Timeout. Poll-basiert statt time-hardcoded gibt CI-
|
|
188
|
+
// Geschwindigkeits-Toleranz; flake-frei solange total-source-time
|
|
189
|
+
// (5 × 30ms = 150ms) > poll-Granularitaet (10ms).
|
|
190
|
+
const basePath = await mkdtemp(join(tmpdir(), "kumiko-stream-prop-"));
|
|
191
|
+
try {
|
|
192
|
+
const provider = createLocalProvider(basePath);
|
|
193
|
+
const chunks = [
|
|
194
|
+
new Uint8Array([1]),
|
|
195
|
+
new Uint8Array([2]),
|
|
196
|
+
new Uint8Array([3]),
|
|
197
|
+
new Uint8Array([4]),
|
|
198
|
+
new Uint8Array([5]),
|
|
199
|
+
];
|
|
200
|
+
const writePromise = provider.writeStream("streamed.bin", slowChunks(chunks, 30));
|
|
201
|
+
|
|
202
|
+
// Poll bis tmp existiert ODER final schon fertig (zu schneller CI).
|
|
203
|
+
let hasTmp = false;
|
|
204
|
+
let alreadyDone = false;
|
|
205
|
+
for (let i = 0; i < 20 && !hasTmp && !alreadyDone; i++) {
|
|
206
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
207
|
+
const dirContents = await readdir(basePath);
|
|
208
|
+
hasTmp = dirContents.some((f) => f.endsWith(".tmp"));
|
|
209
|
+
alreadyDone = dirContents.includes("streamed.bin");
|
|
210
|
+
}
|
|
211
|
+
if (alreadyDone && !hasTmp) {
|
|
212
|
+
throw new Error(
|
|
213
|
+
"slowChunks-delay zu kurz fuer CI: writeStream war fertig bevor poll start. " +
|
|
214
|
+
"delayMs erhoehen oder chunks reduzieren.",
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
expect(hasTmp).toBe(true); // tmp existiert WAEHREND yields
|
|
218
|
+
|
|
219
|
+
// Warten auf completion
|
|
220
|
+
await writePromise;
|
|
221
|
+
|
|
222
|
+
// Nach completion: tmp ist weg, final-File ist da
|
|
223
|
+
const finalContents = await readdir(basePath);
|
|
224
|
+
expect(finalContents).toContain("streamed.bin");
|
|
225
|
+
expect(finalContents.filter((f) => f.endsWith(".tmp"))).toEqual([]);
|
|
226
|
+
|
|
227
|
+
const data = await provider.read("streamed.bin");
|
|
228
|
+
expect(Array.from(data)).toEqual([1, 2, 3, 4, 5]);
|
|
229
|
+
} finally {
|
|
230
|
+
await rm(basePath, { recursive: true, force: true });
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
});
|