@effect-agent/platform-cloudflare 0.1.0-beta.39 → 0.1.0-beta.41
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/dist/{bindings-C00ZfjsX.d.mts → bindings-BrVEOkfe.d.mts} +4 -2
- package/dist/browser-quick-action.d.mts +18 -1
- package/dist/browser-quick-action.mjs +14 -1
- package/dist/browser-quick-action.mjs.map +1 -1
- package/dist/browser-rest-capture.d.mts +17 -2
- package/dist/browser-rest-capture.mjs +12 -1
- package/dist/browser-rest-capture.mjs.map +1 -1
- package/dist/browser-rest-crawl.mjs +2 -2
- package/dist/browser-rest-crawl.mjs.map +1 -1
- package/dist/index.d.mts +204 -22
- package/dist/index.mjs +161 -7
- package/dist/index.mjs.map +1 -1
- package/dist/interactive-browser.d.mts +17 -1
- package/dist/interactive-browser.mjs +18 -6
- package/dist/interactive-browser.mjs.map +1 -1
- package/dist/{prepared-admission-D5hQ-ofV.mjs → prepared-admission-BKp_Upw2.mjs} +16 -4
- package/dist/prepared-admission-BKp_Upw2.mjs.map +1 -0
- package/dist/scheduling.d.mts +1 -1
- package/dist/scheduling.mjs +1 -1
- package/dist/subscriptions.d.mts +1 -1
- package/dist/subscriptions.mjs +2 -2
- package/dist/subscriptions.mjs.map +1 -1
- package/package.json +10 -10
- package/src/bindings.ts +5 -1
- package/src/browser-quick-action.ts +31 -0
- package/src/browser-rest-capture.ts +27 -0
- package/src/client.ts +21 -7
- package/src/code-mode-executor.ts +19 -0
- package/src/index.ts +2 -0
- package/src/interactive-browser.ts +29 -1
- package/src/memory.ts +317 -0
- package/dist/prepared-admission-D5hQ-ofV.mjs.map +0 -1
|
@@ -418,3 +418,30 @@ export const browserRestWorkersAiCaptureLayer = (
|
|
|
418
418
|
return PageCapture.of({ capture: makeCapture(client, options, workersAi) });
|
|
419
419
|
}),
|
|
420
420
|
);
|
|
421
|
+
|
|
422
|
+
/** Explicit REST credentials and optional authorization for separately billed extraction. */
|
|
423
|
+
export interface CloudflareBrowserRestOptions extends BrowserRestCaptureOptions {
|
|
424
|
+
readonly workersAi?: BrowserQuickActionWorkersAiPolicy;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/** Node-safe WebCapture handler assembly; the application supplies its HttpClient. */
|
|
428
|
+
export const CloudflareBrowserRest = {
|
|
429
|
+
/**
|
|
430
|
+
* Supports capture, scrape, and extraction definitions. Only PageCapture is supplied;
|
|
431
|
+
* handler errors and other services remain visible. Extraction fails closed without
|
|
432
|
+
* workersAi. Existing host policies, response limits, and scoped cleanup are unchanged.
|
|
433
|
+
*/
|
|
434
|
+
layer: <A, E, R>(
|
|
435
|
+
definition: { readonly handlers: Layer.Layer<A, E, R> },
|
|
436
|
+
options: CloudflareBrowserRestOptions,
|
|
437
|
+
): Layer.Layer<A, E, Exclude<R, PageCapture> | HttpClient.HttpClient> => {
|
|
438
|
+
const capture =
|
|
439
|
+
options.workersAi === undefined
|
|
440
|
+
? browserRestCaptureLayer(options)
|
|
441
|
+
: browserRestWorkersAiCaptureLayer(options).pipe(
|
|
442
|
+
Layer.provide(BrowserQuickActionWorkersAi.layer(options.workersAi)),
|
|
443
|
+
);
|
|
444
|
+
|
|
445
|
+
return definition.handlers.pipe(Layer.provide(capture));
|
|
446
|
+
},
|
|
447
|
+
};
|
package/src/client.ts
CHANGED
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
type DurableSubmitAgent,
|
|
31
31
|
type DurableSubmitOptions,
|
|
32
32
|
} from "@effect-agent/thread";
|
|
33
|
+
import { BrowserCrypto } from "@effect/platform-browser";
|
|
33
34
|
import { Context, Crypto, Duration, Effect, Layer, Schema } from "effect";
|
|
34
35
|
import { RpcTracing } from "effect-cf";
|
|
35
36
|
|
|
@@ -387,6 +388,20 @@ export class CloudflareThreadClient extends Context.Service<
|
|
|
387
388
|
) => Effect.Effect<UnknownResolutionIntent, ClientUnknownFailure>;
|
|
388
389
|
}
|
|
389
390
|
>()("@effect-agent/platform-cloudflare/CloudflareThreadClient") {
|
|
391
|
+
/**
|
|
392
|
+
* Assemble the client from a resolved namespace and the platform Crypto implementation.
|
|
393
|
+
* rpcTracing is the binding name and remains opt-in. Caller authentication, principals,
|
|
394
|
+
* idempotency keys, and definition digests are still explicit submission inputs.
|
|
395
|
+
*/
|
|
396
|
+
static layerFromBinding(options: {
|
|
397
|
+
readonly namespace: DurableObjectNamespace<ThreadObjectRpc>;
|
|
398
|
+
readonly rpcTracing?: string;
|
|
399
|
+
}): Layer.Layer<CloudflareThreadClient> {
|
|
400
|
+
return CloudflareThreadClient.layer.pipe(
|
|
401
|
+
Layer.provide([ThreadObjectNamespace.layer(options.namespace, options), BrowserCrypto.layer]),
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
|
|
390
405
|
static readonly layer: Layer.Layer<
|
|
391
406
|
CloudflareThreadClient,
|
|
392
407
|
never,
|
|
@@ -424,13 +439,12 @@ export class CloudflareThreadClient extends Context.Service<
|
|
|
424
439
|
}),
|
|
425
440
|
});
|
|
426
441
|
return yield* decodeHostResponse(raw).pipe(
|
|
427
|
-
Effect.mapError(
|
|
428
|
-
(
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
}),
|
|
442
|
+
Effect.mapError((error): HostProtocolError =>
|
|
443
|
+
HostProtocolError.make({
|
|
444
|
+
message: boundHostDiagnostic(
|
|
445
|
+
`The ${operation} answer could not be decoded: ${error.message}`,
|
|
446
|
+
),
|
|
447
|
+
}),
|
|
434
448
|
),
|
|
435
449
|
);
|
|
436
450
|
},
|
|
@@ -814,3 +814,22 @@ export const dynamicWorkerCodeExecutorLayer = (
|
|
|
814
814
|
return CodeExecutor.of({ execute: makeExecute(options, clock) });
|
|
815
815
|
}),
|
|
816
816
|
);
|
|
817
|
+
|
|
818
|
+
/** Assemble Code Mode handlers with the isolated Dynamic Worker executor. */
|
|
819
|
+
export const CloudflareCodeMode = {
|
|
820
|
+
/**
|
|
821
|
+
* Provide the selected tool handlers at construction, where Code Mode captures them.
|
|
822
|
+
* Their errors and remaining dependencies stay visible. The definition still owns the
|
|
823
|
+
* allowlist and limits; the runtime supplies the live Tool broker for each scoped pass.
|
|
824
|
+
*/
|
|
825
|
+
layer: <A, E, R, Handlers, HandlerError, HandlerRequirements>(
|
|
826
|
+
definition: { readonly handlers: Layer.Layer<A, E, R> },
|
|
827
|
+
options: DynamicWorkerCodeExecutorOptions & {
|
|
828
|
+
readonly handlers: Layer.Layer<Handlers, HandlerError, HandlerRequirements>;
|
|
829
|
+
},
|
|
830
|
+
) =>
|
|
831
|
+
definition.handlers.pipe(
|
|
832
|
+
Layer.provide(options.handlers),
|
|
833
|
+
Layer.provide(dynamicWorkerCodeExecutorLayer(options)),
|
|
834
|
+
),
|
|
835
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -26,3 +26,5 @@ export * from "./prepared-admission.ts";
|
|
|
26
26
|
export * from "./scheduling.ts";
|
|
27
27
|
export * from "./subscriptions.ts";
|
|
28
28
|
export * from "./code-mode-executor.ts";
|
|
29
|
+
export { CloudflareBrowser, type CloudflareBrowserOptions } from "./browser-quick-action.ts";
|
|
30
|
+
export * from "./memory.ts";
|
|
@@ -44,7 +44,10 @@ import {
|
|
|
44
44
|
type Scope,
|
|
45
45
|
} from "effect";
|
|
46
46
|
|
|
47
|
-
import {
|
|
47
|
+
import {
|
|
48
|
+
BrowserRunSessionLifecycle,
|
|
49
|
+
type BrowserRunLifecycleOptions,
|
|
50
|
+
} from "./browser-session-lifecycle.ts";
|
|
48
51
|
export { BrowserRunCleanupError, BrowserRunSessionLifecycle } from "./browser-session-lifecycle.ts";
|
|
49
52
|
|
|
50
53
|
export const browserRunInteractiveImplementation = SandboxImplementation.make({
|
|
@@ -2145,3 +2148,28 @@ export const browserRunInteractiveLayer = (): Layer.Layer<
|
|
|
2145
2148
|
});
|
|
2146
2149
|
}),
|
|
2147
2150
|
);
|
|
2151
|
+
|
|
2152
|
+
/** Resolved Worker binding, cleanup credentials, and optional initial viewport. */
|
|
2153
|
+
export interface CloudflareInteractiveBrowserOptions extends BrowserRunLifecycleOptions {
|
|
2154
|
+
readonly browser: BrowserRun;
|
|
2155
|
+
readonly viewport?: BrowserRunViewport;
|
|
2156
|
+
}
|
|
2157
|
+
|
|
2158
|
+
const interactiveBindingLayer = (options: CloudflareInteractiveBrowserOptions) =>
|
|
2159
|
+
BrowserRunInteractiveBinding.layer(options).pipe(
|
|
2160
|
+
Layer.provide(BrowserRunSessionLifecycle.layer(options)),
|
|
2161
|
+
);
|
|
2162
|
+
|
|
2163
|
+
/** Worker-only assembly; importing ordinary capture never loads Puppeteer. */
|
|
2164
|
+
export const CloudflareInteractiveBrowser = {
|
|
2165
|
+
/**
|
|
2166
|
+
* Assemble binding, confirmed-session cleanup, and the generic browser service.
|
|
2167
|
+
* HttpClient and construction errors remain visible. Opening a pass still requires
|
|
2168
|
+
* an explicit policy and Scope; no browser is launched during Layer construction.
|
|
2169
|
+
*/
|
|
2170
|
+
layer: (options: CloudflareInteractiveBrowserOptions) =>
|
|
2171
|
+
browserRunInteractiveLayer().pipe(Layer.provide(interactiveBindingLayer(options))),
|
|
2172
|
+
/** Opt into private host controls instead of exposing them through the generic service. */
|
|
2173
|
+
hostLayer: (options: CloudflareInteractiveBrowserOptions) =>
|
|
2174
|
+
browserRunInteractiveHostLayer().pipe(Layer.provide(interactiveBindingLayer(options))),
|
|
2175
|
+
};
|
package/src/memory.ts
ADDED
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type MemoryLookup,
|
|
3
|
+
type MemoryReader,
|
|
4
|
+
type MemoryWrite,
|
|
5
|
+
Memory,
|
|
6
|
+
MemoryAccess,
|
|
7
|
+
MemoryDocument,
|
|
8
|
+
MemoryMutationFailpoint,
|
|
9
|
+
MemoryNamespace,
|
|
10
|
+
MemoryNamespaceAddress,
|
|
11
|
+
MemoryRecallLimits,
|
|
12
|
+
MemoryStorageError,
|
|
13
|
+
MemoryWriter,
|
|
14
|
+
type MemoryIndexSearch,
|
|
15
|
+
type SemanticCandidateLimits,
|
|
16
|
+
type SemanticMemoryProfile,
|
|
17
|
+
} from "@effect-agent/core";
|
|
18
|
+
import {
|
|
19
|
+
type DoMemoryStorageLimits,
|
|
20
|
+
type MemoryOwnerAuthorizer,
|
|
21
|
+
decodeMemoryWire,
|
|
22
|
+
defaultDoMemoryStorageLimits,
|
|
23
|
+
defaultMemoryRpcLimits,
|
|
24
|
+
doMemoryStoreLayerWithFailpoints,
|
|
25
|
+
encodeMemoryWire,
|
|
26
|
+
handleMemoryOwnerRequest,
|
|
27
|
+
MemoryOwnerIdentity,
|
|
28
|
+
MemoryOwnerRequest,
|
|
29
|
+
MemoryOwnerResponse,
|
|
30
|
+
MemoryRpcError,
|
|
31
|
+
MemoryRpcLimits,
|
|
32
|
+
type MemoryOwnerFailure,
|
|
33
|
+
} from "@effect-agent/storage-cloudflare";
|
|
34
|
+
import { Principal } from "@effect-agent/thread";
|
|
35
|
+
import { Clock, Context, Effect, Layer, Schema } from "effect";
|
|
36
|
+
import {
|
|
37
|
+
DurableObject as EffectCfDurableObject,
|
|
38
|
+
DurableObjectState,
|
|
39
|
+
type WorkerEnvironment,
|
|
40
|
+
} from "effect-cf";
|
|
41
|
+
|
|
42
|
+
export interface MemoryObjectRpc extends Rpc.DurableObjectBranded {
|
|
43
|
+
memory(encoded: string): Promise<string>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export class MemoryObjectNamespace extends Context.Service<
|
|
47
|
+
MemoryObjectNamespace,
|
|
48
|
+
{
|
|
49
|
+
readonly namespace: DurableObjectNamespace<MemoryObjectRpc>;
|
|
50
|
+
}
|
|
51
|
+
>()("@effect-agent/platform-cloudflare/MemoryObjectNamespace") {}
|
|
52
|
+
|
|
53
|
+
/** Namespace version and identity are already canonicalized by MemoryNamespace. */
|
|
54
|
+
export const memoryObjectName = (namespace: MemoryNamespace.Any): string => namespace.address;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Effect-native, host-bound memory client. Recall revalidates the entire admitted lookup
|
|
58
|
+
* in one RPC and renders it locally. No retries or per-source splitting occur here.
|
|
59
|
+
* Interrupted callers stop waiting; the owner has its own deadline. A timed-out write
|
|
60
|
+
* may have committed: reconcile by sending the identical operation ID and command.
|
|
61
|
+
*/
|
|
62
|
+
const makeMemoryClient = Effect.fn("CloudflareMemoryClient.make")(function* <
|
|
63
|
+
Namespace extends MemoryNamespace.Any,
|
|
64
|
+
>(
|
|
65
|
+
access: MemoryAccess<Namespace>,
|
|
66
|
+
principal: Principal,
|
|
67
|
+
rpcLimits: MemoryRpcLimits = defaultMemoryRpcLimits,
|
|
68
|
+
) {
|
|
69
|
+
const validated = yield* Schema.decodeUnknownEffect(MemoryRpcLimits)(rpcLimits).pipe(
|
|
70
|
+
Effect.mapError(() => MemoryRpcError.make({ reason: "protocol" })),
|
|
71
|
+
);
|
|
72
|
+
const bound = yield* Schema.decodeUnknownEffect(MemoryAccess.Wire)(access).pipe(
|
|
73
|
+
Effect.mapError(() => MemoryRpcError.make({ reason: "protocol" })),
|
|
74
|
+
);
|
|
75
|
+
principal = yield* Schema.decodeUnknownEffect(Principal)(principal).pipe(
|
|
76
|
+
Effect.mapError(() => MemoryRpcError.make({ reason: "protocol" })),
|
|
77
|
+
);
|
|
78
|
+
const { namespace } = yield* MemoryObjectNamespace;
|
|
79
|
+
const call = Effect.fn("CloudflareMemoryClient.call")(function* (request: MemoryOwnerRequest) {
|
|
80
|
+
const decoded = yield* Schema.decodeUnknownEffect(MemoryOwnerRequest)(request).pipe(
|
|
81
|
+
Effect.mapError(() => MemoryRpcError.make({ reason: "protocol" })),
|
|
82
|
+
);
|
|
83
|
+
const encoded = yield* encodeMemoryWire(MemoryOwnerRequest, decoded, validated.maxRequestBytes);
|
|
84
|
+
const raw = yield* Effect.tryPromise({
|
|
85
|
+
try: () =>
|
|
86
|
+
namespace.get(namespace.idFromName(memoryObjectName(bound.namespace))).memory(encoded),
|
|
87
|
+
catch: () => MemoryRpcError.make({ reason: "unavailable" }),
|
|
88
|
+
});
|
|
89
|
+
const response = yield* decodeMemoryWire(MemoryOwnerResponse, raw, validated.maxResponseBytes);
|
|
90
|
+
if (response._tag === "Failed") return yield* response.failure;
|
|
91
|
+
if (
|
|
92
|
+
!MemoryNamespace.equals(response.access.namespace, bound.namespace) ||
|
|
93
|
+
response.access.scope !== bound.scope
|
|
94
|
+
)
|
|
95
|
+
return yield* MemoryRpcError.make({ reason: "protocol" });
|
|
96
|
+
return response;
|
|
97
|
+
});
|
|
98
|
+
const withinDeadline = <A, E, R>(effect: Effect.Effect<A, E, R>, timeoutMillis: number) =>
|
|
99
|
+
effect.pipe(
|
|
100
|
+
Effect.timeoutOrElse({
|
|
101
|
+
duration: timeoutMillis,
|
|
102
|
+
orElse: () => Effect.fail(MemoryRpcError.make({ reason: "timeout" })),
|
|
103
|
+
}),
|
|
104
|
+
);
|
|
105
|
+
const revalidate = Effect.fn("CloudflareMemoryClient.revalidate")(function* (
|
|
106
|
+
lookup: MemoryLookup,
|
|
107
|
+
limits: MemoryRecallLimits,
|
|
108
|
+
) {
|
|
109
|
+
limits = yield* Schema.decodeUnknownEffect(MemoryRecallLimits)(limits).pipe(
|
|
110
|
+
Effect.mapError(() => MemoryRpcError.make({ reason: "protocol" })),
|
|
111
|
+
);
|
|
112
|
+
const timeoutMillis = Math.min(validated.timeoutMillis, limits.timeoutMillis);
|
|
113
|
+
return yield* Effect.gen(function* () {
|
|
114
|
+
const response = yield* call({
|
|
115
|
+
_tag: "Revalidate",
|
|
116
|
+
version: 1,
|
|
117
|
+
access: bound,
|
|
118
|
+
principal,
|
|
119
|
+
lookup,
|
|
120
|
+
limits,
|
|
121
|
+
deadlineMillis: (yield* Clock.currentTimeMillis) + timeoutMillis,
|
|
122
|
+
});
|
|
123
|
+
if (response._tag !== "Lookup") return yield* MemoryRpcError.make({ reason: "protocol" });
|
|
124
|
+
return response.lookup;
|
|
125
|
+
}).pipe((effect) => withinDeadline(effect, timeoutMillis));
|
|
126
|
+
});
|
|
127
|
+
const change = Effect.fn("CloudflareMemoryClient.change")(function* (
|
|
128
|
+
write: MemoryWrite<Namespace>,
|
|
129
|
+
) {
|
|
130
|
+
if (!MemoryNamespace.equals(write.key.namespace, bound.namespace))
|
|
131
|
+
return yield* MemoryRpcError.make({ reason: "denied" });
|
|
132
|
+
return yield* Effect.gen(function* () {
|
|
133
|
+
const response = yield* call({
|
|
134
|
+
_tag: "Change",
|
|
135
|
+
version: 1,
|
|
136
|
+
access: bound,
|
|
137
|
+
principal,
|
|
138
|
+
write,
|
|
139
|
+
deadlineMillis: (yield* Clock.currentTimeMillis) + validated.timeoutMillis,
|
|
140
|
+
});
|
|
141
|
+
if (response._tag !== "Changed" || response.document.key.id !== write.key.id)
|
|
142
|
+
return yield* MemoryRpcError.make({ reason: "protocol" });
|
|
143
|
+
return yield* MemoryDocument.restore(access.namespace, response.document);
|
|
144
|
+
}).pipe((effect) => withinDeadline(effect, validated.timeoutMillis));
|
|
145
|
+
});
|
|
146
|
+
const revalidateSemantic = Effect.fn("CloudflareMemoryClient.revalidateSemantic")(function* (
|
|
147
|
+
found: MemoryIndexSearch<Namespace>,
|
|
148
|
+
profile: SemanticMemoryProfile,
|
|
149
|
+
limits: SemanticCandidateLimits,
|
|
150
|
+
) {
|
|
151
|
+
return yield* Effect.gen(function* () {
|
|
152
|
+
const response = yield* call({
|
|
153
|
+
_tag: "RevalidateSemantic",
|
|
154
|
+
version: 1,
|
|
155
|
+
access: bound,
|
|
156
|
+
principal,
|
|
157
|
+
found,
|
|
158
|
+
profile,
|
|
159
|
+
limits,
|
|
160
|
+
deadlineMillis: (yield* Clock.currentTimeMillis) + validated.timeoutMillis,
|
|
161
|
+
});
|
|
162
|
+
if (response._tag !== "Semantic") return yield* MemoryRpcError.make({ reason: "protocol" });
|
|
163
|
+
return response.result;
|
|
164
|
+
}).pipe((effect) => withinDeadline(effect, validated.timeoutMillis));
|
|
165
|
+
});
|
|
166
|
+
/**
|
|
167
|
+
* Revalidate in one owner RPC, then render whole passages within the caller's budget.
|
|
168
|
+
* The bound source is essential: unavailable/stale results and matches that cannot fit
|
|
169
|
+
* fail instead of silently producing empty context. No-match remains successful.
|
|
170
|
+
* The single outcome has sourceId "memory". No embedding or candidate search is performed.
|
|
171
|
+
* Use revalidate with Memory.recall for multiple readers sharing one output budget.
|
|
172
|
+
*/
|
|
173
|
+
const recall = Effect.fn("CloudflareMemoryClient.recall")(function* (
|
|
174
|
+
lookup: MemoryLookup,
|
|
175
|
+
limits: MemoryRecallLimits,
|
|
176
|
+
estimateTokens?: (text: string) => number,
|
|
177
|
+
) {
|
|
178
|
+
return yield* Memory.recall(
|
|
179
|
+
[{ id: "memory", essential: true, read: revalidate(lookup, limits) }],
|
|
180
|
+
limits,
|
|
181
|
+
estimateTokens,
|
|
182
|
+
);
|
|
183
|
+
});
|
|
184
|
+
return { recall, revalidate, revalidateSemantic, change };
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
export const CloudflareMemoryClient = {
|
|
188
|
+
/** Bind access and principal using the MemoryObjectNamespace supplied by the application. */
|
|
189
|
+
make: makeMemoryClient,
|
|
190
|
+
/** Use a resolved Worker or Durable Object binding without manual service provisioning. */
|
|
191
|
+
fromBinding: Effect.fn("CloudflareMemoryClient.fromBinding")(function* <
|
|
192
|
+
Namespace extends MemoryNamespace.Any,
|
|
193
|
+
>(
|
|
194
|
+
binding: DurableObjectNamespace<MemoryObjectRpc>,
|
|
195
|
+
options: {
|
|
196
|
+
readonly access: MemoryAccess<Namespace>;
|
|
197
|
+
readonly principal: Principal;
|
|
198
|
+
readonly rpcLimits?: MemoryRpcLimits;
|
|
199
|
+
},
|
|
200
|
+
) {
|
|
201
|
+
return yield* makeMemoryClient(options.access, options.principal, options.rpcLimits).pipe(
|
|
202
|
+
Effect.provideService(MemoryObjectNamespace, { namespace: binding }),
|
|
203
|
+
);
|
|
204
|
+
}),
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Optional activity-processor destination. Keeps domain write errors intact; transport,
|
|
209
|
+
* authorization and deadline failures become the existing MemoryStorageError contract.
|
|
210
|
+
* Receipts remain authoritative, including after caller interruption or lost replies.
|
|
211
|
+
*/
|
|
212
|
+
export const cloudflareMemoryWriterLayer = (
|
|
213
|
+
access: MemoryAccess,
|
|
214
|
+
principal: Principal,
|
|
215
|
+
limits: MemoryRpcLimits = defaultMemoryRpcLimits,
|
|
216
|
+
) =>
|
|
217
|
+
Layer.effect(
|
|
218
|
+
MemoryWriter,
|
|
219
|
+
Effect.gen(function* () {
|
|
220
|
+
const client = yield* CloudflareMemoryClient.make(access, principal, limits);
|
|
221
|
+
return MemoryWriter.fromAdapter({
|
|
222
|
+
change: (write) =>
|
|
223
|
+
client.change(write).pipe(
|
|
224
|
+
Effect.catchTag("MemoryRpcError", (error) =>
|
|
225
|
+
Effect.fail(
|
|
226
|
+
MemoryStorageError.make({
|
|
227
|
+
operation: `memory RPC ${error.reason}`,
|
|
228
|
+
reason:
|
|
229
|
+
error.reason === "unavailable" || error.reason === "timeout"
|
|
230
|
+
? "unavailable"
|
|
231
|
+
: "invalid-input",
|
|
232
|
+
}),
|
|
233
|
+
),
|
|
234
|
+
),
|
|
235
|
+
Effect.catchTag(["MemoryRecallError", "MemoryIndexError", "SemanticMemoryError"], () =>
|
|
236
|
+
Effect.fail(
|
|
237
|
+
MemoryStorageError.make({ operation: "memory RPC response", reason: "corrupt" }),
|
|
238
|
+
),
|
|
239
|
+
),
|
|
240
|
+
),
|
|
241
|
+
});
|
|
242
|
+
}),
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
type OwnerServices = MemoryReader | MemoryWriter | MemoryOwnerAuthorizer | MemoryOwnerIdentity;
|
|
246
|
+
export interface MemoryObjectInstance extends InstanceType<
|
|
247
|
+
EffectCfDurableObject.DurableObjectClass<Record<never, never>, OwnerServices>
|
|
248
|
+
> {
|
|
249
|
+
memory(encoded: string): Promise<string>;
|
|
250
|
+
}
|
|
251
|
+
export interface MemoryObjectClass {
|
|
252
|
+
new (ctx: globalThis.DurableObjectState, env: Cloudflare.Env): MemoryObjectInstance;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Dedicated SQLite owner, independent of Thread lifetimes. The host binds authorization
|
|
257
|
+
* after restoring its namespace definition from MemoryOwnerIdentity. Do not retain
|
|
258
|
+
* cleanup-scoped resources in the host Layer; it lives for the DO incarnation.
|
|
259
|
+
*/
|
|
260
|
+
const makeMemoryObject = <E>(
|
|
261
|
+
host: Layer.Layer<
|
|
262
|
+
MemoryOwnerAuthorizer,
|
|
263
|
+
E,
|
|
264
|
+
MemoryOwnerIdentity | DurableObjectState.DurableObjectState | WorkerEnvironment
|
|
265
|
+
>,
|
|
266
|
+
options: {
|
|
267
|
+
readonly storageLimits?: DoMemoryStorageLimits;
|
|
268
|
+
readonly rpcLimits?: MemoryRpcLimits;
|
|
269
|
+
readonly failpoints?: Layer.Layer<
|
|
270
|
+
MemoryMutationFailpoint,
|
|
271
|
+
never,
|
|
272
|
+
DurableObjectState.DurableObjectState
|
|
273
|
+
>;
|
|
274
|
+
} = {},
|
|
275
|
+
): MemoryObjectClass => {
|
|
276
|
+
const identity = Layer.effect(
|
|
277
|
+
MemoryOwnerIdentity,
|
|
278
|
+
Effect.gen(function* () {
|
|
279
|
+
const state = yield* DurableObjectState.DurableObjectState;
|
|
280
|
+
const address = yield* Schema.decodeUnknownEffect(MemoryNamespaceAddress)(
|
|
281
|
+
state.raw.id.name,
|
|
282
|
+
).pipe(Effect.mapError(() => MemoryRpcError.make({ reason: "denied" })));
|
|
283
|
+
return { namespace: MemoryNamespace.Any.make({ address }) };
|
|
284
|
+
}),
|
|
285
|
+
);
|
|
286
|
+
const store = Layer.unwrap(
|
|
287
|
+
Effect.map(DurableObjectState.DurableObjectState, (state) =>
|
|
288
|
+
doMemoryStoreLayerWithFailpoints(
|
|
289
|
+
state.raw.storage,
|
|
290
|
+
options.storageLimits ?? defaultDoMemoryStorageLimits,
|
|
291
|
+
),
|
|
292
|
+
),
|
|
293
|
+
).pipe(Layer.provide(options.failpoints ?? MemoryMutationFailpoint.layer));
|
|
294
|
+
const application = Layer.merge(store, host).pipe(Layer.provideMerge(identity));
|
|
295
|
+
const runtime: Layer.Layer<
|
|
296
|
+
OwnerServices,
|
|
297
|
+
E | MemoryOwnerFailure,
|
|
298
|
+
DurableObjectState.DurableObjectState | WorkerEnvironment
|
|
299
|
+
> = Layer.effectContext(
|
|
300
|
+
Effect.gen(function* () {
|
|
301
|
+
const state = yield* DurableObjectState.DurableObjectState;
|
|
302
|
+
const scope = yield* Effect.scope;
|
|
303
|
+
yield* Schema.decodeUnknownEffect(MemoryRpcLimits)(
|
|
304
|
+
options.rpcLimits ?? defaultMemoryRpcLimits,
|
|
305
|
+
).pipe(Effect.mapError(() => MemoryRpcError.make({ reason: "protocol" })));
|
|
306
|
+
return yield* state.blockConcurrencyWhile(Layer.buildWithScope(application, scope));
|
|
307
|
+
}),
|
|
308
|
+
);
|
|
309
|
+
return EffectCfDurableObject.make(runtime, {
|
|
310
|
+
rpc: { memory: (encoded: string) => handleMemoryOwnerRequest(encoded, options.rpcLimits) },
|
|
311
|
+
});
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
export const MemoryObject = {
|
|
315
|
+
/** Build the SQLite Durable Object class with the application's owner authorization Layer. */
|
|
316
|
+
make: makeMemoryObject,
|
|
317
|
+
};
|