@yolk-sdk/sandbox 0.1.0-canary.18

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.
Files changed (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +60 -0
  3. package/dist/agent.d.mts +48 -0
  4. package/dist/agent.d.mts.map +1 -0
  5. package/dist/agent.mjs +173 -0
  6. package/dist/agent.mjs.map +1 -0
  7. package/dist/errors.d.mts +44 -0
  8. package/dist/errors.d.mts.map +1 -0
  9. package/dist/errors.mjs +40 -0
  10. package/dist/errors.mjs.map +1 -0
  11. package/dist/index.d.mts +7 -0
  12. package/dist/index.mjs +7 -0
  13. package/dist/lifecycle.d.mts +52 -0
  14. package/dist/lifecycle.d.mts.map +1 -0
  15. package/dist/lifecycle.mjs +118 -0
  16. package/dist/lifecycle.mjs.map +1 -0
  17. package/dist/model.d.mts +84 -0
  18. package/dist/model.d.mts.map +1 -0
  19. package/dist/model.mjs +72 -0
  20. package/dist/model.mjs.map +1 -0
  21. package/dist/name.d.mts +5 -0
  22. package/dist/name.d.mts.map +1 -0
  23. package/dist/name.mjs +19 -0
  24. package/dist/name.mjs.map +1 -0
  25. package/dist/service.d.mts +15 -0
  26. package/dist/service.d.mts.map +1 -0
  27. package/dist/service.mjs +7 -0
  28. package/dist/service.mjs.map +1 -0
  29. package/dist/state-store.d.mts +18 -0
  30. package/dist/state-store.d.mts.map +1 -0
  31. package/dist/state-store.mjs +7 -0
  32. package/dist/state-store.mjs.map +1 -0
  33. package/dist/testing/index.d.mts +21 -0
  34. package/dist/testing/index.d.mts.map +1 -0
  35. package/dist/testing/index.mjs +25 -0
  36. package/dist/testing/index.mjs.map +1 -0
  37. package/dist/vercel/client.d.mts +70 -0
  38. package/dist/vercel/client.d.mts.map +1 -0
  39. package/dist/vercel/client.mjs +113 -0
  40. package/dist/vercel/client.mjs.map +1 -0
  41. package/dist/vercel/index.d.mts +3 -0
  42. package/dist/vercel/index.mjs +3 -0
  43. package/dist/vercel/layer.d.mts +23 -0
  44. package/dist/vercel/layer.d.mts.map +1 -0
  45. package/dist/vercel/layer.mjs +250 -0
  46. package/dist/vercel/layer.mjs.map +1 -0
  47. package/package.json +72 -0
  48. package/src/agent.ts +304 -0
  49. package/src/errors.ts +71 -0
  50. package/src/index.ts +52 -0
  51. package/src/lifecycle.ts +186 -0
  52. package/src/model.ts +122 -0
  53. package/src/name.ts +23 -0
  54. package/src/service.ts +19 -0
  55. package/src/state-store.ts +17 -0
  56. package/src/testing/index.ts +63 -0
  57. package/src/vercel/client.ts +223 -0
  58. package/src/vercel/index.ts +24 -0
  59. package/src/vercel/layer.ts +396 -0
package/src/errors.ts ADDED
@@ -0,0 +1,71 @@
1
+ import * as Schema from 'effect/Schema'
2
+
3
+ export const SandboxInputErrorCause = Schema.Literals([
4
+ 'empty_command',
5
+ 'invalid_cwd',
6
+ 'invalid_timeout'
7
+ ])
8
+ export type SandboxInputErrorCause = typeof SandboxInputErrorCause.Type
9
+
10
+ export class SandboxInputError extends Schema.TaggedErrorClass<SandboxInputError>()(
11
+ 'SandboxInputError',
12
+ {
13
+ cause: SandboxInputErrorCause,
14
+ message: Schema.String
15
+ }
16
+ ) {}
17
+
18
+ export class SandboxConfigError extends Schema.TaggedErrorClass<SandboxConfigError>()(
19
+ 'SandboxConfigError',
20
+ {
21
+ message: Schema.String,
22
+ cause: Schema.String
23
+ }
24
+ ) {}
25
+
26
+ export class SandboxExpiredError extends Schema.TaggedErrorClass<SandboxExpiredError>()(
27
+ 'SandboxExpiredError',
28
+ {
29
+ message: Schema.String,
30
+ expiredAtMs: Schema.Number
31
+ }
32
+ ) {}
33
+
34
+ export class SandboxStateError extends Schema.TaggedErrorClass<SandboxStateError>()(
35
+ 'SandboxStateError',
36
+ {
37
+ message: Schema.String,
38
+ cause: Schema.String,
39
+ underlying: Schema.optional(Schema.Unknown)
40
+ }
41
+ ) {}
42
+
43
+ export class SandboxStateStoreError extends Schema.TaggedErrorClass<SandboxStateStoreError>()(
44
+ 'SandboxStateStoreError',
45
+ {
46
+ message: Schema.String,
47
+ operation: Schema.String,
48
+ underlying: Schema.optional(Schema.Unknown)
49
+ }
50
+ ) {}
51
+
52
+ export class SandboxProviderError extends Schema.TaggedErrorClass<SandboxProviderError>()(
53
+ 'SandboxProviderError',
54
+ {
55
+ provider: Schema.Literal('vercel'),
56
+ operation: Schema.String,
57
+ message: Schema.String,
58
+ underlying: Schema.optional(Schema.Unknown)
59
+ }
60
+ ) {}
61
+
62
+ export type SandboxError =
63
+ | SandboxInputError
64
+ | SandboxConfigError
65
+ | SandboxExpiredError
66
+ | SandboxStateError
67
+ | SandboxStateStoreError
68
+ | SandboxProviderError
69
+
70
+ export const unknownToMessage = (error: unknown) =>
71
+ error instanceof Error ? error.message : String(error)
package/src/index.ts ADDED
@@ -0,0 +1,52 @@
1
+ export {
2
+ backgroundSandboxProbeMs,
3
+ defaultSandboxCommandTimeoutMs,
4
+ defaultSandboxIdleTtlMs,
5
+ defaultSandboxInitialSource,
6
+ defaultSandboxLifecycle,
7
+ defaultSandboxMaxLifetimeMs,
8
+ defaultSandboxPorts,
9
+ defaultSandboxWorkspaceRoot,
10
+ maxSandboxCommandTimeoutMs,
11
+ normalizeWorkspaceCwd,
12
+ sandboxCommandTimeoutMs,
13
+ sandboxLifecycleOrDefault,
14
+ sandboxSourceOrDefault,
15
+ sandboxStateDecision,
16
+ sandboxTimeoutExtendDeltaMs,
17
+ sandboxToolOutputLimit,
18
+ touchSandboxState,
19
+ validateSandboxCommand
20
+ } from './lifecycle.ts'
21
+ export { makeVercelSandboxName } from './name.ts'
22
+ export {
23
+ DisposableSandboxLifecycle,
24
+ EmptySandboxInitialSource,
25
+ GitSandboxInitialSource,
26
+ PersistentSandboxLifecycle,
27
+ SandboxCommandInput,
28
+ SandboxCommandResult,
29
+ SandboxInitialSource,
30
+ SandboxLifecycle,
31
+ SandboxPreviewUrl,
32
+ SandboxResources,
33
+ SandboxSnapshotRetention,
34
+ SandboxState,
35
+ SnapshotSandboxInitialSource,
36
+ TarballSandboxInitialSource,
37
+ VercelSandboxState
38
+ } from './model.ts'
39
+ export { Sandbox } from './service.ts'
40
+ export { SandboxStateStore } from './state-store.ts'
41
+ export {
42
+ SandboxConfigError,
43
+ SandboxExpiredError,
44
+ SandboxInputError,
45
+ SandboxInputErrorCause,
46
+ SandboxProviderError,
47
+ SandboxStateError,
48
+ SandboxStateStoreError
49
+ } from './errors.ts'
50
+ export type { SandboxError } from './errors.ts'
51
+ export type { SandboxApi } from './service.ts'
52
+ export type { SandboxStateStoreApi } from './state-store.ts'
@@ -0,0 +1,186 @@
1
+ import { Effect, Option } from 'effect'
2
+ import { SandboxInputError } from './errors.ts'
3
+ import {
4
+ DisposableSandboxLifecycle,
5
+ EmptySandboxInitialSource,
6
+ type SandboxInitialSource,
7
+ type SandboxLifecycle,
8
+ type SandboxState,
9
+ VercelSandboxState
10
+ } from './model.ts'
11
+
12
+ export const defaultSandboxIdleTtlMs = 30 * 60_000
13
+ export const defaultSandboxMaxLifetimeMs = 45 * 60_000
14
+ export const defaultSandboxCommandTimeoutMs = 120_000
15
+ export const maxSandboxCommandTimeoutMs = 600_000
16
+ export const backgroundSandboxProbeMs = 2_000
17
+ export const sandboxToolOutputLimit = 50_000
18
+ export const defaultSandboxPorts: ReadonlyArray<number> = [3000, 5173, 4321, 8000]
19
+ export const defaultSandboxWorkspaceRoot = '/vercel/sandbox'
20
+
21
+ export const defaultSandboxLifecycle = DisposableSandboxLifecycle.make({
22
+ idleTtlMs: defaultSandboxIdleTtlMs,
23
+ maxLifetimeMs: defaultSandboxMaxLifetimeMs
24
+ })
25
+
26
+ export const defaultSandboxInitialSource = EmptySandboxInitialSource.make({})
27
+
28
+ export type SandboxRecreateReason = 'idle_expired' | 'max_expired' | 'name_mismatch'
29
+
30
+ export type SandboxStateDecision =
31
+ | {
32
+ readonly _tag: 'UseExisting'
33
+ readonly state: SandboxState
34
+ }
35
+ | {
36
+ readonly _tag: 'Create'
37
+ readonly workspaceReset: boolean
38
+ readonly reason?: SandboxRecreateReason
39
+ }
40
+
41
+ const positiveOr = (value: number | undefined, fallback: number) =>
42
+ value === undefined || !Number.isFinite(value) || value <= 0 ? fallback : Math.floor(value)
43
+
44
+ export const sandboxLifecycleOrDefault = (lifecycle?: SandboxLifecycle): SandboxLifecycle =>
45
+ lifecycle ?? defaultSandboxLifecycle
46
+
47
+ export const sandboxSourceOrDefault = (source?: SandboxInitialSource): SandboxInitialSource =>
48
+ source ?? defaultSandboxInitialSource
49
+
50
+ export const sandboxCommandTimeoutMs = (timeoutMs?: number) =>
51
+ Math.min(maxSandboxCommandTimeoutMs, positiveOr(timeoutMs, defaultSandboxCommandTimeoutMs))
52
+
53
+ export const validateSandboxCommand = (command: string) => {
54
+ if (command.trim().length === 0) {
55
+ return Effect.fail(
56
+ new SandboxInputError({
57
+ cause: 'empty_command',
58
+ message: 'Sandbox command must not be empty'
59
+ })
60
+ )
61
+ }
62
+
63
+ return Effect.succeed(command)
64
+ }
65
+
66
+ export const normalizeWorkspaceCwd = (cwd?: string | null): Effect.Effect<string, SandboxInputError> => {
67
+ if (cwd === undefined || cwd === null || cwd.trim().length === 0) {
68
+ return Effect.succeed('.')
69
+ }
70
+
71
+ const trimmed = cwd.trim()
72
+
73
+ if (trimmed.startsWith('/') || trimmed.includes('\0')) {
74
+ return Effect.fail(
75
+ new SandboxInputError({
76
+ cause: 'invalid_cwd',
77
+ message: 'cwd must be workspace-relative'
78
+ })
79
+ )
80
+ }
81
+
82
+ const parts: Array<string> = []
83
+
84
+ for (const segment of trimmed.split('/')) {
85
+ if (segment.length === 0 || segment === '.') {
86
+ continue
87
+ }
88
+
89
+ if (segment === '..') {
90
+ const previous = parts.pop()
91
+ if (previous === undefined) {
92
+ return Effect.fail(
93
+ new SandboxInputError({
94
+ cause: 'invalid_cwd',
95
+ message: 'cwd must not escape the workspace'
96
+ })
97
+ )
98
+ }
99
+ continue
100
+ }
101
+
102
+ parts.push(segment)
103
+ }
104
+
105
+ return Effect.succeed(parts.length === 0 ? '.' : parts.join('/'))
106
+ }
107
+
108
+ export const absoluteSandboxCwd = (workspaceRoot: string, normalizedCwd: string) =>
109
+ normalizedCwd === '.' ? workspaceRoot : `${workspaceRoot}/${normalizedCwd}`
110
+
111
+ export const initialSandboxState = (input: {
112
+ readonly name: string
113
+ readonly nowMs: number
114
+ readonly lifecycle: SandboxLifecycle
115
+ }) => {
116
+ switch (input.lifecycle._tag) {
117
+ case 'Disposable':
118
+ return VercelSandboxState.make({
119
+ name: input.name,
120
+ createdAtMs: input.nowMs,
121
+ lastUsedAtMs: input.nowMs,
122
+ expiresAtMs: input.nowMs + input.lifecycle.idleTtlMs,
123
+ maxExpiresAtMs: input.nowMs + input.lifecycle.maxLifetimeMs
124
+ })
125
+ case 'Persistent':
126
+ return VercelSandboxState.make({
127
+ name: input.name,
128
+ createdAtMs: input.nowMs,
129
+ lastUsedAtMs: input.nowMs,
130
+ expiresAtMs: input.nowMs + input.lifecycle.idleTtlMs,
131
+ maxExpiresAtMs: Number.MAX_SAFE_INTEGER
132
+ })
133
+ }
134
+ }
135
+
136
+ export const touchSandboxState = (input: {
137
+ readonly state: SandboxState
138
+ readonly nowMs: number
139
+ readonly lifecycle: SandboxLifecycle
140
+ }) => {
141
+ switch (input.lifecycle._tag) {
142
+ case 'Disposable':
143
+ return VercelSandboxState.make({
144
+ ...input.state,
145
+ lastUsedAtMs: input.nowMs,
146
+ expiresAtMs: Math.min(input.nowMs + input.lifecycle.idleTtlMs, input.state.maxExpiresAtMs)
147
+ })
148
+ case 'Persistent':
149
+ return VercelSandboxState.make({
150
+ ...input.state,
151
+ lastUsedAtMs: input.nowMs,
152
+ expiresAtMs: input.nowMs + input.lifecycle.idleTtlMs
153
+ })
154
+ }
155
+ }
156
+
157
+ export const sandboxTimeoutExtendDeltaMs = (input: {
158
+ readonly before: SandboxState
159
+ readonly after: SandboxState
160
+ }) => Math.max(0, input.after.expiresAtMs - input.before.expiresAtMs)
161
+
162
+ export const sandboxStateDecision = (input: {
163
+ readonly state: Option.Option<SandboxState>
164
+ readonly name: string
165
+ readonly nowMs: number
166
+ }): SandboxStateDecision => {
167
+ if (Option.isNone(input.state)) {
168
+ return { _tag: 'Create', workspaceReset: false }
169
+ }
170
+
171
+ const state = input.state.value
172
+
173
+ if (state.name !== input.name) {
174
+ return { _tag: 'Create', workspaceReset: true, reason: 'name_mismatch' }
175
+ }
176
+
177
+ if (input.nowMs >= state.maxExpiresAtMs) {
178
+ return { _tag: 'Create', workspaceReset: true, reason: 'max_expired' }
179
+ }
180
+
181
+ if (input.nowMs >= state.expiresAtMs) {
182
+ return { _tag: 'Create', workspaceReset: true, reason: 'idle_expired' }
183
+ }
184
+
185
+ return { _tag: 'UseExisting', state }
186
+ }
package/src/model.ts ADDED
@@ -0,0 +1,122 @@
1
+ import * as Schema from 'effect/Schema'
2
+
3
+ const NonEmptyTrimmedString = Schema.Trimmed.pipe(Schema.check(Schema.isNonEmpty()))
4
+ const NonNegativeInteger = Schema.Int.pipe(Schema.check(Schema.isGreaterThanOrEqualTo(0)))
5
+ const PositiveInteger = Schema.Int.pipe(Schema.check(Schema.isGreaterThan(0)))
6
+ const PositiveNumber = Schema.Number.pipe(Schema.check(Schema.isGreaterThan(0)))
7
+
8
+ export class SandboxCommandInput extends Schema.Class<SandboxCommandInput>(
9
+ 'SandboxCommandInput'
10
+ )({
11
+ command: Schema.String,
12
+ cwd: Schema.optional(Schema.String),
13
+ stdin: Schema.optional(Schema.String),
14
+ timeoutMs: Schema.optional(PositiveInteger),
15
+ background: Schema.optional(Schema.Boolean)
16
+ }) {}
17
+
18
+ export class SandboxPreviewUrl extends Schema.Class<SandboxPreviewUrl>('SandboxPreviewUrl')({
19
+ port: PositiveInteger,
20
+ url: NonEmptyTrimmedString
21
+ }) {}
22
+
23
+ export class VercelSandboxState extends Schema.TaggedClass<VercelSandboxState>()(
24
+ 'Vercel',
25
+ {
26
+ name: NonEmptyTrimmedString,
27
+ createdAtMs: NonNegativeInteger,
28
+ lastUsedAtMs: NonNegativeInteger,
29
+ expiresAtMs: NonNegativeInteger,
30
+ maxExpiresAtMs: NonNegativeInteger
31
+ }
32
+ ) {}
33
+
34
+ export const SandboxState = Schema.Union([VercelSandboxState])
35
+ export type SandboxState = typeof SandboxState.Type
36
+
37
+ export class SandboxCommandResult extends Schema.Class<SandboxCommandResult>(
38
+ 'SandboxCommandResult'
39
+ )({
40
+ exitCode: Schema.NullOr(Schema.Int),
41
+ stdout: Schema.String,
42
+ stderr: Schema.String,
43
+ durationMs: NonNegativeInteger,
44
+ timedOut: Schema.Boolean,
45
+ workspaceReset: Schema.Boolean,
46
+ backgroundId: Schema.optional(NonEmptyTrimmedString),
47
+ previewUrls: Schema.Array(SandboxPreviewUrl),
48
+ state: SandboxState
49
+ }) {}
50
+
51
+ export class DisposableSandboxLifecycle extends Schema.TaggedClass<DisposableSandboxLifecycle>()(
52
+ 'Disposable',
53
+ {
54
+ idleTtlMs: PositiveInteger,
55
+ maxLifetimeMs: PositiveInteger
56
+ }
57
+ ) {}
58
+
59
+ export class SandboxSnapshotRetention extends Schema.Class<SandboxSnapshotRetention>(
60
+ 'SandboxSnapshotRetention'
61
+ )({
62
+ count: PositiveInteger,
63
+ expirationMs: Schema.optional(NonNegativeInteger),
64
+ deleteEvicted: Schema.optional(Schema.Boolean)
65
+ }) {}
66
+
67
+ export class PersistentSandboxLifecycle extends Schema.TaggedClass<PersistentSandboxLifecycle>()(
68
+ 'Persistent',
69
+ {
70
+ idleTtlMs: PositiveInteger,
71
+ snapshotExpirationMs: Schema.optional(NonNegativeInteger),
72
+ keepLastSnapshots: Schema.optional(SandboxSnapshotRetention)
73
+ }
74
+ ) {}
75
+
76
+ export const SandboxLifecycle = Schema.Union([
77
+ DisposableSandboxLifecycle,
78
+ PersistentSandboxLifecycle
79
+ ])
80
+ export type SandboxLifecycle = typeof SandboxLifecycle.Type
81
+
82
+ export class EmptySandboxInitialSource extends Schema.TaggedClass<EmptySandboxInitialSource>()(
83
+ 'Empty',
84
+ {}
85
+ ) {}
86
+
87
+ export class SnapshotSandboxInitialSource extends Schema.TaggedClass<SnapshotSandboxInitialSource>()(
88
+ 'Snapshot',
89
+ {
90
+ snapshotId: NonEmptyTrimmedString
91
+ }
92
+ ) {}
93
+
94
+ export class GitSandboxInitialSource extends Schema.TaggedClass<GitSandboxInitialSource>()(
95
+ 'Git',
96
+ {
97
+ url: NonEmptyTrimmedString,
98
+ username: Schema.optional(NonEmptyTrimmedString),
99
+ password: Schema.optional(NonEmptyTrimmedString),
100
+ depth: Schema.optional(PositiveInteger),
101
+ revision: Schema.optional(NonEmptyTrimmedString)
102
+ }
103
+ ) {}
104
+
105
+ export class TarballSandboxInitialSource extends Schema.TaggedClass<TarballSandboxInitialSource>()(
106
+ 'Tarball',
107
+ {
108
+ url: NonEmptyTrimmedString
109
+ }
110
+ ) {}
111
+
112
+ export const SandboxInitialSource = Schema.Union([
113
+ EmptySandboxInitialSource,
114
+ SnapshotSandboxInitialSource,
115
+ GitSandboxInitialSource,
116
+ TarballSandboxInitialSource
117
+ ])
118
+ export type SandboxInitialSource = typeof SandboxInitialSource.Type
119
+
120
+ export class SandboxResources extends Schema.Class<SandboxResources>('SandboxResources')({
121
+ vcpus: PositiveNumber
122
+ }) {}
package/src/name.ts ADDED
@@ -0,0 +1,23 @@
1
+ const fnvOffsetBasis32 = 0x811c9dc5
2
+ const fnvPrime32 = 0x01000193
3
+
4
+ const fnv1a32 = (value: string, seed: number) => {
5
+ let hash = seed
6
+
7
+ for (const character of value) {
8
+ hash ^= character.codePointAt(0) ?? 0
9
+ hash = Math.imul(hash, fnvPrime32)
10
+ }
11
+
12
+ return (hash >>> 0).toString(36).padStart(7, '0')
13
+ }
14
+
15
+ const stableHash = (value: string) => {
16
+ const first = fnv1a32(value, fnvOffsetBasis32)
17
+ const second = fnv1a32([...value].reverse().join(''), fnvOffsetBasis32 ^ 0x9e3779b9)
18
+
19
+ return `${first}${second}`
20
+ }
21
+
22
+ export const makeVercelSandboxName = (sandboxSessionId: string) =>
23
+ `sandbox-${stableHash(sandboxSessionId)}`
package/src/service.ts ADDED
@@ -0,0 +1,19 @@
1
+ import { Context } from 'effect'
2
+ import type { Effect, Option } from 'effect'
3
+ import type {
4
+ SandboxError,
5
+ SandboxProviderError,
6
+ SandboxStateError,
7
+ SandboxStateStoreError
8
+ } from './errors.ts'
9
+ import type { SandboxCommandInput, SandboxCommandResult, SandboxState } from './model.ts'
10
+
11
+ export type SandboxApi = {
12
+ readonly run: (input: SandboxCommandInput) => Effect.Effect<SandboxCommandResult, SandboxError>
13
+ readonly currentState: Effect.Effect<Option.Option<SandboxState>, SandboxStateStoreError | SandboxStateError>
14
+ readonly delete: Effect.Effect<void, SandboxProviderError | SandboxStateStoreError | SandboxStateError>
15
+ }
16
+
17
+ export class Sandbox extends Context.Service<Sandbox, SandboxApi>()(
18
+ '@yolk-sdk/sandbox/Sandbox'
19
+ ) {}
@@ -0,0 +1,17 @@
1
+ import { Context } from 'effect'
2
+ import type { Effect, Option } from 'effect'
3
+ import type { SandboxStateStoreError } from './errors.ts'
4
+ import type { SandboxState } from './model.ts'
5
+
6
+ export type SandboxStateStoreApi = {
7
+ readonly load: (sandboxSessionId: string) => Effect.Effect<Option.Option<SandboxState>, SandboxStateStoreError>
8
+ readonly save: (input: {
9
+ readonly sandboxSessionId: string
10
+ readonly state: SandboxState
11
+ }) => Effect.Effect<void, SandboxStateStoreError>
12
+ readonly clear: (sandboxSessionId: string) => Effect.Effect<void, SandboxStateStoreError>
13
+ }
14
+
15
+ export class SandboxStateStore extends Context.Service<SandboxStateStore, SandboxStateStoreApi>()(
16
+ '@yolk-sdk/sandbox/SandboxStateStore'
17
+ ) {}
@@ -0,0 +1,63 @@
1
+ import { Effect, Layer, Option, Ref } from 'effect'
2
+ import type { SandboxCommandInput, SandboxCommandResult, SandboxState } from '../model.ts'
3
+ import { Sandbox } from '../service.ts'
4
+ import { SandboxStateStore } from '../state-store.ts'
5
+ import type { SandboxError } from '../errors.ts'
6
+
7
+ export type FakeSandboxRun = (
8
+ input: SandboxCommandInput
9
+ ) => Effect.Effect<SandboxCommandResult, SandboxError>
10
+
11
+ export type FakeSandboxOptions = {
12
+ readonly run: FakeSandboxRun
13
+ readonly initialState?: SandboxState
14
+ }
15
+
16
+ export const makeFakeSandboxLayer = (options: FakeSandboxOptions) =>
17
+ Layer.effect(
18
+ Sandbox,
19
+ Ref.make(Option.fromNullishOr(options.initialState)).pipe(
20
+ Effect.map(stateRef =>
21
+ Sandbox.of({
22
+ run: input =>
23
+ options.run(input).pipe(
24
+ Effect.tap(result => Ref.set(stateRef, Option.some(result.state)))
25
+ ),
26
+ currentState: Ref.get(stateRef),
27
+ delete: Ref.set(stateRef, Option.none())
28
+ })
29
+ )
30
+ )
31
+ )
32
+
33
+ export type InMemorySandboxStateStoreEntry = {
34
+ readonly sandboxSessionId: string
35
+ readonly state: SandboxState
36
+ }
37
+
38
+ export const makeInMemorySandboxStateStoreLayer = (
39
+ initial: ReadonlyArray<InMemorySandboxStateStoreEntry> = []
40
+ ) =>
41
+ Layer.effect(
42
+ SandboxStateStore,
43
+ Effect.gen(function* () {
44
+ const states = yield* Ref.make(
45
+ new Map(initial.map(entry => [entry.sandboxSessionId, entry.state]))
46
+ )
47
+
48
+ return SandboxStateStore.of({
49
+ load: sandboxSessionId =>
50
+ Ref.get(states).pipe(
51
+ Effect.map(current => Option.fromNullishOr(current.get(sandboxSessionId)))
52
+ ),
53
+ save: input =>
54
+ Ref.update(states, current => new Map([...current, [input.sandboxSessionId, input.state]])),
55
+ clear: sandboxSessionId =>
56
+ Ref.update(states, current => {
57
+ const next = new Map(current)
58
+ next.delete(sandboxSessionId)
59
+ return next
60
+ })
61
+ })
62
+ })
63
+ )