@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.
- package/LICENSE +21 -0
- package/README.md +60 -0
- package/dist/agent.d.mts +48 -0
- package/dist/agent.d.mts.map +1 -0
- package/dist/agent.mjs +173 -0
- package/dist/agent.mjs.map +1 -0
- package/dist/errors.d.mts +44 -0
- package/dist/errors.d.mts.map +1 -0
- package/dist/errors.mjs +40 -0
- package/dist/errors.mjs.map +1 -0
- package/dist/index.d.mts +7 -0
- package/dist/index.mjs +7 -0
- package/dist/lifecycle.d.mts +52 -0
- package/dist/lifecycle.d.mts.map +1 -0
- package/dist/lifecycle.mjs +118 -0
- package/dist/lifecycle.mjs.map +1 -0
- package/dist/model.d.mts +84 -0
- package/dist/model.d.mts.map +1 -0
- package/dist/model.mjs +72 -0
- package/dist/model.mjs.map +1 -0
- package/dist/name.d.mts +5 -0
- package/dist/name.d.mts.map +1 -0
- package/dist/name.mjs +19 -0
- package/dist/name.mjs.map +1 -0
- package/dist/service.d.mts +15 -0
- package/dist/service.d.mts.map +1 -0
- package/dist/service.mjs +7 -0
- package/dist/service.mjs.map +1 -0
- package/dist/state-store.d.mts +18 -0
- package/dist/state-store.d.mts.map +1 -0
- package/dist/state-store.mjs +7 -0
- package/dist/state-store.mjs.map +1 -0
- package/dist/testing/index.d.mts +21 -0
- package/dist/testing/index.d.mts.map +1 -0
- package/dist/testing/index.mjs +25 -0
- package/dist/testing/index.mjs.map +1 -0
- package/dist/vercel/client.d.mts +70 -0
- package/dist/vercel/client.d.mts.map +1 -0
- package/dist/vercel/client.mjs +113 -0
- package/dist/vercel/client.mjs.map +1 -0
- package/dist/vercel/index.d.mts +3 -0
- package/dist/vercel/index.mjs +3 -0
- package/dist/vercel/layer.d.mts +23 -0
- package/dist/vercel/layer.d.mts.map +1 -0
- package/dist/vercel/layer.mjs +250 -0
- package/dist/vercel/layer.mjs.map +1 -0
- package/package.json +72 -0
- package/src/agent.ts +304 -0
- package/src/errors.ts +71 -0
- package/src/index.ts +52 -0
- package/src/lifecycle.ts +186 -0
- package/src/model.ts +122 -0
- package/src/name.ts +23 -0
- package/src/service.ts +19 -0
- package/src/state-store.ts +17 -0
- package/src/testing/index.ts +63 -0
- package/src/vercel/client.ts +223 -0
- package/src/vercel/index.ts +24 -0
- 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'
|
package/src/lifecycle.ts
ADDED
|
@@ -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
|
+
)
|