mustardscript 0.1.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.
Files changed (99) hide show
  1. package/Cargo.lock +1579 -0
  2. package/Cargo.toml +40 -0
  3. package/LICENSE +201 -0
  4. package/README.md +828 -0
  5. package/SECURITY.md +34 -0
  6. package/crates/mustard/Cargo.toml +31 -0
  7. package/crates/mustard/src/cancellation.rs +28 -0
  8. package/crates/mustard/src/diagnostic.rs +145 -0
  9. package/crates/mustard/src/ir.rs +435 -0
  10. package/crates/mustard/src/lib.rs +21 -0
  11. package/crates/mustard/src/limits.rs +22 -0
  12. package/crates/mustard/src/parser/expressions.rs +723 -0
  13. package/crates/mustard/src/parser/mod.rs +115 -0
  14. package/crates/mustard/src/parser/operators.rs +105 -0
  15. package/crates/mustard/src/parser/patterns.rs +123 -0
  16. package/crates/mustard/src/parser/scope.rs +107 -0
  17. package/crates/mustard/src/parser/statements.rs +298 -0
  18. package/crates/mustard/src/parser/tests/acceptance.rs +339 -0
  19. package/crates/mustard/src/parser/tests/mod.rs +2 -0
  20. package/crates/mustard/src/parser/tests/rejections.rs +107 -0
  21. package/crates/mustard/src/runtime/accounting.rs +613 -0
  22. package/crates/mustard/src/runtime/api.rs +192 -0
  23. package/crates/mustard/src/runtime/async_runtime/mod.rs +5 -0
  24. package/crates/mustard/src/runtime/async_runtime/promises.rs +246 -0
  25. package/crates/mustard/src/runtime/async_runtime/reactions.rs +400 -0
  26. package/crates/mustard/src/runtime/async_runtime/scheduler.rs +224 -0
  27. package/crates/mustard/src/runtime/builtins/arrays.rs +1205 -0
  28. package/crates/mustard/src/runtime/builtins/collections.rs +573 -0
  29. package/crates/mustard/src/runtime/builtins/install.rs +501 -0
  30. package/crates/mustard/src/runtime/builtins/intl.rs +553 -0
  31. package/crates/mustard/src/runtime/builtins/mod.rs +25 -0
  32. package/crates/mustard/src/runtime/builtins/objects.rs +405 -0
  33. package/crates/mustard/src/runtime/builtins/primitives.rs +859 -0
  34. package/crates/mustard/src/runtime/builtins/promises.rs +335 -0
  35. package/crates/mustard/src/runtime/builtins/regexp.rs +356 -0
  36. package/crates/mustard/src/runtime/builtins/strings.rs +803 -0
  37. package/crates/mustard/src/runtime/builtins/support.rs +561 -0
  38. package/crates/mustard/src/runtime/bytecode.rs +123 -0
  39. package/crates/mustard/src/runtime/compiler/assignments.rs +690 -0
  40. package/crates/mustard/src/runtime/compiler/bindings.rs +92 -0
  41. package/crates/mustard/src/runtime/compiler/context.rs +46 -0
  42. package/crates/mustard/src/runtime/compiler/control.rs +342 -0
  43. package/crates/mustard/src/runtime/compiler/expressions.rs +372 -0
  44. package/crates/mustard/src/runtime/compiler/mod.rs +173 -0
  45. package/crates/mustard/src/runtime/compiler/statements.rs +459 -0
  46. package/crates/mustard/src/runtime/conversions/boundary.rs +293 -0
  47. package/crates/mustard/src/runtime/conversions/coercions.rs +217 -0
  48. package/crates/mustard/src/runtime/conversions/errors.rs +118 -0
  49. package/crates/mustard/src/runtime/conversions/mod.rs +14 -0
  50. package/crates/mustard/src/runtime/conversions/operators.rs +334 -0
  51. package/crates/mustard/src/runtime/env.rs +355 -0
  52. package/crates/mustard/src/runtime/exceptions.rs +377 -0
  53. package/crates/mustard/src/runtime/gc.rs +595 -0
  54. package/crates/mustard/src/runtime/mod.rs +318 -0
  55. package/crates/mustard/src/runtime/properties.rs +1762 -0
  56. package/crates/mustard/src/runtime/serialization.rs +127 -0
  57. package/crates/mustard/src/runtime/shared.rs +108 -0
  58. package/crates/mustard/src/runtime/snapshot_validation_tests.rs +93 -0
  59. package/crates/mustard/src/runtime/state.rs +652 -0
  60. package/crates/mustard/src/runtime/tests/async_host.rs +104 -0
  61. package/crates/mustard/src/runtime/tests/collections.rs +50 -0
  62. package/crates/mustard/src/runtime/tests/diagnostics.rs +36 -0
  63. package/crates/mustard/src/runtime/tests/exceptions.rs +122 -0
  64. package/crates/mustard/src/runtime/tests/execution.rs +553 -0
  65. package/crates/mustard/src/runtime/tests/gc.rs +533 -0
  66. package/crates/mustard/src/runtime/tests/mod.rs +56 -0
  67. package/crates/mustard/src/runtime/tests/serialization.rs +170 -0
  68. package/crates/mustard/src/runtime/validation/bytecode.rs +484 -0
  69. package/crates/mustard/src/runtime/validation/mod.rs +14 -0
  70. package/crates/mustard/src/runtime/validation/policy.rs +94 -0
  71. package/crates/mustard/src/runtime/validation/snapshot.rs +406 -0
  72. package/crates/mustard/src/runtime/validation/walk.rs +206 -0
  73. package/crates/mustard/src/runtime/vm.rs +1016 -0
  74. package/crates/mustard/src/span.rs +22 -0
  75. package/crates/mustard/src/structured.rs +107 -0
  76. package/crates/mustard-bridge/Cargo.toml +17 -0
  77. package/crates/mustard-bridge/src/codec.rs +46 -0
  78. package/crates/mustard-bridge/src/dto.rs +99 -0
  79. package/crates/mustard-bridge/src/lib.rs +12 -0
  80. package/crates/mustard-bridge/src/operations.rs +142 -0
  81. package/crates/mustard-node/Cargo.toml +24 -0
  82. package/crates/mustard-node/build.rs +3 -0
  83. package/crates/mustard-node/src/lib.rs +236 -0
  84. package/crates/mustard-sidecar/Cargo.toml +21 -0
  85. package/crates/mustard-sidecar/src/lib.rs +134 -0
  86. package/crates/mustard-sidecar/src/main.rs +36 -0
  87. package/dist/index.js +20 -0
  88. package/dist/install.js +117 -0
  89. package/dist/lib/cancellation.js +124 -0
  90. package/dist/lib/errors.js +46 -0
  91. package/dist/lib/executor.js +555 -0
  92. package/dist/lib/policy.js +292 -0
  93. package/dist/lib/progress.js +356 -0
  94. package/dist/lib/runtime.js +109 -0
  95. package/dist/lib/structured.js +286 -0
  96. package/dist/native-loader.js +227 -0
  97. package/index.d.ts +23 -0
  98. package/mustard.d.ts +220 -0
  99. package/package.json +97 -0
@@ -0,0 +1,236 @@
1
+ use base64::{Engine as _, engine::general_purpose::STANDARD};
2
+ use hmac::{Hmac, Mac};
3
+ use rand::random;
4
+ use sha2::{Digest, Sha256};
5
+ use std::collections::{HashMap, HashSet};
6
+ use std::sync::{
7
+ Arc, Mutex, OnceLock,
8
+ atomic::{AtomicBool, Ordering},
9
+ };
10
+
11
+ use mustard::CancellationToken;
12
+ use mustard_bridge::{
13
+ ResumeDto, SnapshotPolicyDto, StartOptionsDto, compile_program_bytes, decode_program,
14
+ encode_json, inspect_snapshot_bytes, parse_json, resume_program as bridge_resume_program,
15
+ start_program as bridge_start_program,
16
+ };
17
+ use napi::bindgen_prelude::Buffer;
18
+ use napi::{Error, Result};
19
+ use napi_derive::napi;
20
+
21
+ type HmacSha256 = Hmac<Sha256>;
22
+
23
+ fn cancellation_tokens() -> &'static Mutex<HashMap<String, Arc<AtomicBool>>> {
24
+ static TOKENS: OnceLock<Mutex<HashMap<String, Arc<AtomicBool>>>> = OnceLock::new();
25
+ TOKENS.get_or_init(|| Mutex::new(HashMap::new()))
26
+ }
27
+
28
+ fn used_progress_snapshots() -> &'static Mutex<HashSet<String>> {
29
+ static TOKENS: OnceLock<Mutex<HashSet<String>>> = OnceLock::new();
30
+ TOKENS.get_or_init(|| Mutex::new(HashSet::new()))
31
+ }
32
+
33
+ fn next_cancellation_token_id(tokens: &HashMap<String, Arc<AtomicBool>>) -> String {
34
+ loop {
35
+ let candidate = format!("cancel-{:032x}", random::<u128>());
36
+ if !tokens.contains_key(&candidate) {
37
+ return candidate;
38
+ }
39
+ }
40
+ }
41
+
42
+ fn lookup_cancellation_token(token_id: Option<String>) -> Result<Option<CancellationToken>> {
43
+ let Some(token_id) = token_id else {
44
+ return Ok(None);
45
+ };
46
+ let tokens = cancellation_tokens()
47
+ .lock()
48
+ .map_err(|_| to_napi_error("cancellation token registry is poisoned"))?;
49
+ let shared = tokens
50
+ .get(&token_id)
51
+ .cloned()
52
+ .ok_or_else(|| to_napi_error(format!("unknown cancellation token `{token_id}`")))?;
53
+ Ok(Some(CancellationToken::from_shared(shared)))
54
+ }
55
+
56
+ fn to_napi_error(error: impl std::fmt::Display) -> Error {
57
+ Error::from_reason(error.to_string())
58
+ }
59
+
60
+ fn snapshot_identity_hex(snapshot: &[u8]) -> String {
61
+ let digest = Sha256::digest(snapshot);
62
+ let mut encoded = String::with_capacity(digest.len() * 2);
63
+ for byte in digest {
64
+ use std::fmt::Write as _;
65
+ let _ = write!(&mut encoded, "{byte:02x}");
66
+ }
67
+ encoded
68
+ }
69
+
70
+ fn snapshot_key_digest_hex(snapshot_key: &[u8]) -> String {
71
+ let digest = Sha256::digest(snapshot_key);
72
+ let mut encoded = String::with_capacity(digest.len() * 2);
73
+ for byte in digest {
74
+ use std::fmt::Write as _;
75
+ let _ = write!(&mut encoded, "{byte:02x}");
76
+ }
77
+ encoded
78
+ }
79
+
80
+ fn encode_snapshot_token(snapshot_id: &str, snapshot_key: &[u8]) -> Result<String> {
81
+ let mut mac = HmacSha256::new_from_slice(snapshot_key)
82
+ .map_err(|_| to_napi_error("invalid snapshot key"))?;
83
+ mac.update(snapshot_id.as_bytes());
84
+ let digest = mac.finalize().into_bytes();
85
+ let mut token = String::with_capacity(digest.len() * 2);
86
+ for byte in digest {
87
+ use std::fmt::Write as _;
88
+ let _ = write!(&mut token, "{byte:02x}");
89
+ }
90
+ Ok(token)
91
+ }
92
+
93
+ fn assert_authenticated_snapshot(snapshot: &[u8], policy: &SnapshotPolicyDto) -> Result<()> {
94
+ let snapshot_id = policy
95
+ .snapshot_id
96
+ .as_deref()
97
+ .ok_or_else(|| to_napi_error("raw snapshot restore requires snapshot_id"))?;
98
+ let snapshot_key_base64 = policy
99
+ .snapshot_key_base64
100
+ .as_deref()
101
+ .ok_or_else(|| to_napi_error("raw snapshot restore requires snapshot_key_base64"))?;
102
+ let snapshot_token = policy
103
+ .snapshot_token
104
+ .as_deref()
105
+ .ok_or_else(|| to_napi_error("raw snapshot restore requires snapshot_token"))?;
106
+ let snapshot_key_digest = policy
107
+ .snapshot_key_digest
108
+ .as_deref()
109
+ .ok_or_else(|| to_napi_error("raw snapshot restore requires snapshot_key_digest"))?;
110
+ let snapshot_key = STANDARD
111
+ .decode(snapshot_key_base64)
112
+ .map_err(|_| to_napi_error("snapshot_key_base64 must be valid base64"))?;
113
+ let expected_snapshot_id = snapshot_identity_hex(snapshot);
114
+ if expected_snapshot_id != snapshot_id {
115
+ return Err(to_napi_error(
116
+ "raw snapshot restore rejected a tampered or unauthenticated snapshot",
117
+ ));
118
+ }
119
+ if snapshot_key_digest_hex(&snapshot_key) != snapshot_key_digest {
120
+ return Err(to_napi_error(
121
+ "raw snapshot restore rejected a mismatched snapshot key digest",
122
+ ));
123
+ }
124
+ let expected = encode_snapshot_token(snapshot_id, &snapshot_key)?;
125
+ if expected != snapshot_token {
126
+ return Err(to_napi_error(
127
+ "raw snapshot restore rejected a tampered or unauthenticated snapshot",
128
+ ));
129
+ }
130
+ Ok(())
131
+ }
132
+
133
+ #[napi]
134
+ pub fn compile_program(source: String) -> Result<Buffer> {
135
+ let bytes = compile_program_bytes(&source).map_err(to_napi_error)?;
136
+ Ok(Buffer::from(bytes))
137
+ }
138
+
139
+ #[napi]
140
+ pub fn create_cancellation_token() -> Result<String> {
141
+ let mut tokens = cancellation_tokens()
142
+ .lock()
143
+ .map_err(|_| to_napi_error("cancellation token registry is poisoned"))?;
144
+ let token_id = next_cancellation_token_id(&tokens);
145
+ tokens.insert(token_id.clone(), Arc::new(AtomicBool::new(false)));
146
+ Ok(token_id)
147
+ }
148
+
149
+ #[napi]
150
+ pub fn cancel_cancellation_token(token_id: String) -> Result<()> {
151
+ let tokens = cancellation_tokens()
152
+ .lock()
153
+ .map_err(|_| to_napi_error("cancellation token registry is poisoned"))?;
154
+ let token = tokens
155
+ .get(&token_id)
156
+ .ok_or_else(|| to_napi_error(format!("unknown cancellation token `{token_id}`")))?;
157
+ token.store(true, Ordering::SeqCst);
158
+ Ok(())
159
+ }
160
+
161
+ #[napi]
162
+ pub fn release_cancellation_token(token_id: String) -> Result<()> {
163
+ let mut tokens = cancellation_tokens()
164
+ .lock()
165
+ .map_err(|_| to_napi_error("cancellation token registry is poisoned"))?;
166
+ tokens.remove(&token_id);
167
+ Ok(())
168
+ }
169
+
170
+ #[napi]
171
+ pub fn snapshot_identity(snapshot: Buffer) -> Result<String> {
172
+ Ok(snapshot_identity_hex(snapshot.as_ref()))
173
+ }
174
+
175
+ #[napi]
176
+ pub fn is_progress_snapshot_used(snapshot_identity: String) -> Result<bool> {
177
+ let tokens = used_progress_snapshots()
178
+ .lock()
179
+ .map_err(|_| to_napi_error("progress snapshot registry is poisoned"))?;
180
+ Ok(tokens.contains(&snapshot_identity))
181
+ }
182
+
183
+ #[napi]
184
+ pub fn claim_progress_snapshot(snapshot_identity: String) -> Result<bool> {
185
+ let mut tokens = used_progress_snapshots()
186
+ .lock()
187
+ .map_err(|_| to_napi_error("progress snapshot registry is poisoned"))?;
188
+ Ok(tokens.insert(snapshot_identity))
189
+ }
190
+
191
+ #[napi]
192
+ pub fn release_progress_snapshot(snapshot_identity: String) -> Result<()> {
193
+ let mut tokens = used_progress_snapshots()
194
+ .lock()
195
+ .map_err(|_| to_napi_error("progress snapshot registry is poisoned"))?;
196
+ tokens.remove(&snapshot_identity);
197
+ Ok(())
198
+ }
199
+
200
+ #[napi]
201
+ pub fn start_program(
202
+ program: Buffer,
203
+ options_json: String,
204
+ cancellation_token_id: Option<String>,
205
+ ) -> Result<String> {
206
+ let program = decode_program(program.as_ref()).map_err(to_napi_error)?;
207
+ let options: StartOptionsDto = parse_json(&options_json).map_err(to_napi_error)?;
208
+ let cancellation_token = lookup_cancellation_token(cancellation_token_id)?;
209
+ let step =
210
+ bridge_start_program(&program, options, cancellation_token).map_err(to_napi_error)?;
211
+ encode_json(&step).map_err(to_napi_error)
212
+ }
213
+
214
+ #[napi]
215
+ pub fn inspect_snapshot(snapshot: Buffer, policy_json: String) -> Result<String> {
216
+ let policy: SnapshotPolicyDto = parse_json(&policy_json).map_err(to_napi_error)?;
217
+ assert_authenticated_snapshot(snapshot.as_ref(), &policy)?;
218
+ let inspection = inspect_snapshot_bytes(snapshot.as_ref(), policy).map_err(to_napi_error)?;
219
+ encode_json(&inspection).map_err(to_napi_error)
220
+ }
221
+
222
+ #[napi]
223
+ pub fn resume_program(
224
+ snapshot: Buffer,
225
+ payload_json: String,
226
+ policy_json: String,
227
+ cancellation_token_id: Option<String>,
228
+ ) -> Result<String> {
229
+ let payload: ResumeDto = parse_json(&payload_json).map_err(to_napi_error)?;
230
+ let policy: SnapshotPolicyDto = parse_json(&policy_json).map_err(to_napi_error)?;
231
+ assert_authenticated_snapshot(snapshot.as_ref(), &policy)?;
232
+ let cancellation_token = lookup_cancellation_token(cancellation_token_id)?;
233
+ let step = bridge_resume_program(snapshot.as_ref(), payload, policy, cancellation_token)
234
+ .map_err(to_napi_error)?;
235
+ encode_json(&step).map_err(to_napi_error)
236
+ }
@@ -0,0 +1,21 @@
1
+ [package]
2
+ name = "mustard-sidecar"
3
+ version.workspace = true
4
+ edition.workspace = true
5
+ license.workspace = true
6
+ authors.workspace = true
7
+ repository.workspace = true
8
+ description = "Sidecar process for the MustardScript runtime"
9
+
10
+ [dependencies]
11
+ anyhow.workspace = true
12
+ mustard-bridge = { path = "../mustard-bridge" }
13
+ serde.workspace = true
14
+ serde_json.workspace = true
15
+ tokio.workspace = true
16
+
17
+ [dev-dependencies]
18
+ base64 = "0.22.1"
19
+ hmac.workspace = true
20
+ mustard = { path = "../mustard" }
21
+ sha2.workspace = true
@@ -0,0 +1,134 @@
1
+ use anyhow::{Context, Result};
2
+ use mustard_bridge::{
3
+ ResumeDto, SnapshotPolicyDto, StartOptionsDto, StepDto, compile_program_bytes, decode_base64,
4
+ decode_program_base64, encode_bytes_base64, resume_program, start_program,
5
+ };
6
+ use serde::{Deserialize, Serialize};
7
+
8
+ pub const PROTOCOL_VERSION: u32 = 1;
9
+ pub const MAX_REQUEST_LINE_BYTES: usize = 1024 * 1024;
10
+
11
+ #[derive(Debug, Serialize, Deserialize)]
12
+ #[serde(tag = "method", rename_all = "snake_case")]
13
+ enum Request {
14
+ Compile {
15
+ protocol_version: u32,
16
+ id: u64,
17
+ source: String,
18
+ },
19
+ Start {
20
+ protocol_version: u32,
21
+ id: u64,
22
+ program_base64: String,
23
+ options: StartOptionsDto,
24
+ },
25
+ Resume {
26
+ protocol_version: u32,
27
+ id: u64,
28
+ snapshot_base64: String,
29
+ policy: Box<SnapshotPolicyDto>,
30
+ payload: Box<ResumeDto>,
31
+ },
32
+ }
33
+
34
+ #[derive(Debug, Serialize, Deserialize)]
35
+ struct Response {
36
+ protocol_version: u32,
37
+ id: u64,
38
+ ok: bool,
39
+ #[serde(skip_serializing_if = "Option::is_none")]
40
+ result: Option<ResponsePayload>,
41
+ #[serde(skip_serializing_if = "Option::is_none")]
42
+ error: Option<String>,
43
+ }
44
+
45
+ #[derive(Debug, Serialize, Deserialize)]
46
+ #[serde(tag = "kind", rename_all = "snake_case")]
47
+ enum ResponsePayload {
48
+ Program { program_base64: String },
49
+ Step { step: StepDto },
50
+ }
51
+
52
+ fn handle(request: Request) -> Response {
53
+ let id = match &request {
54
+ Request::Compile { id, .. } | Request::Start { id, .. } | Request::Resume { id, .. } => *id,
55
+ };
56
+ let protocol_version = match &request {
57
+ Request::Compile {
58
+ protocol_version, ..
59
+ }
60
+ | Request::Start {
61
+ protocol_version, ..
62
+ }
63
+ | Request::Resume {
64
+ protocol_version, ..
65
+ } => *protocol_version,
66
+ };
67
+
68
+ if protocol_version != PROTOCOL_VERSION {
69
+ return Response {
70
+ protocol_version: PROTOCOL_VERSION,
71
+ id,
72
+ ok: false,
73
+ result: None,
74
+ error: Some(format!(
75
+ "unsupported sidecar protocol version {protocol_version}; expected {PROTOCOL_VERSION}"
76
+ )),
77
+ };
78
+ }
79
+
80
+ let result: Result<ResponsePayload> = match request {
81
+ Request::Compile { source, .. } => (|| {
82
+ let bytes = compile_program_bytes(&source)?;
83
+ Ok(ResponsePayload::Program {
84
+ program_base64: encode_bytes_base64(&bytes),
85
+ })
86
+ })(),
87
+ Request::Start {
88
+ program_base64,
89
+ options,
90
+ ..
91
+ } => (|| {
92
+ let program = decode_program_base64(&program_base64)?;
93
+ let step = start_program(&program, options, None)?;
94
+ Ok(ResponsePayload::Step { step })
95
+ })(),
96
+ Request::Resume {
97
+ snapshot_base64,
98
+ policy,
99
+ payload,
100
+ ..
101
+ } => (|| {
102
+ let snapshot_bytes = decode_base64(&snapshot_base64)?;
103
+ let step = resume_program(&snapshot_bytes, *payload, *policy, None)?;
104
+ Ok(ResponsePayload::Step { step })
105
+ })(),
106
+ };
107
+
108
+ match result {
109
+ Ok(result) => Response {
110
+ protocol_version: PROTOCOL_VERSION,
111
+ id,
112
+ ok: true,
113
+ result: Some(result),
114
+ error: None,
115
+ },
116
+ Err(error) => Response {
117
+ protocol_version: PROTOCOL_VERSION,
118
+ id,
119
+ ok: false,
120
+ result: None,
121
+ error: Some(error.to_string()),
122
+ },
123
+ }
124
+ }
125
+
126
+ pub fn handle_request_line(line: &str) -> Result<Option<String>> {
127
+ if line.trim().is_empty() {
128
+ return Ok(None);
129
+ }
130
+ let request: Request = serde_json::from_str(line).context("invalid request")?;
131
+ let response = handle(request);
132
+ let encoded = serde_json::to_string(&response).context("failed to encode response")?;
133
+ Ok(Some(encoded))
134
+ }
@@ -0,0 +1,36 @@
1
+ use std::io::{self, BufRead, Write};
2
+
3
+ use anyhow::{Context, Result};
4
+ use mustard_sidecar::{MAX_REQUEST_LINE_BYTES, handle_request_line};
5
+
6
+ fn main() -> Result<()> {
7
+ let stdin = io::stdin();
8
+ let mut stdin = stdin.lock();
9
+ let mut stdout = io::stdout().lock();
10
+ let mut line = Vec::new();
11
+ loop {
12
+ line.clear();
13
+ let read = stdin
14
+ .read_until(b'\n', &mut line)
15
+ .context("failed to read request line")?;
16
+ if read == 0 {
17
+ break;
18
+ }
19
+ if line.len() > MAX_REQUEST_LINE_BYTES + 1 {
20
+ anyhow::bail!("request line exceeds maximum size of {MAX_REQUEST_LINE_BYTES} bytes");
21
+ }
22
+ if line.ends_with(b"\n") {
23
+ line.pop();
24
+ if line.ends_with(b"\r") {
25
+ line.pop();
26
+ }
27
+ }
28
+ let line = String::from_utf8(line.clone()).context("request line must be valid utf-8")?;
29
+ let Some(response) = handle_request_line(&line)? else {
30
+ continue;
31
+ };
32
+ writeln!(&mut stdout, "{response}").context("failed to terminate response line")?;
33
+ stdout.flush().context("failed to flush response")?;
34
+ }
35
+ Ok(())
36
+ }
package/dist/index.js ADDED
@@ -0,0 +1,20 @@
1
+ 'use strict';
2
+
3
+ const { loadNative } = require('./native-loader.js');
4
+ const { createExecutorApi } = require('./lib/executor.js');
5
+ const { MustardError } = require('./lib/errors.js');
6
+ const { createProgressApi } = require('./lib/progress.js');
7
+ const { createMustardClass } = require('./lib/runtime.js');
8
+
9
+ const native = loadNative();
10
+ const { Progress, materializeStep, parseStep } = createProgressApi(native);
11
+ const Mustard = createMustardClass({ native, materializeStep, parseStep });
12
+ const { InMemoryMustardExecutorStore, MustardExecutor } = createExecutorApi({ Mustard, Progress });
13
+
14
+ module.exports = {
15
+ InMemoryMustardExecutorStore,
16
+ MustardError,
17
+ Mustard,
18
+ MustardExecutor,
19
+ Progress,
20
+ };
@@ -0,0 +1,117 @@
1
+ 'use strict';
2
+
3
+ const { execFileSync } = require('node:child_process');
4
+ const fs = require('node:fs');
5
+ const path = require('node:path');
6
+ const {
7
+ resolvePrebuiltPackage,
8
+ getCurrentPrebuiltTarget,
9
+ getLocalBuildOutputFile,
10
+ } = require('./native-loader.js');
11
+
12
+ const packageRoot = path.basename(__dirname) === 'dist' ? path.dirname(__dirname) : __dirname;
13
+
14
+ let prebuilt = null;
15
+ let prebuiltError = null;
16
+ try {
17
+ prebuilt = resolvePrebuiltPackage();
18
+ } catch (error) {
19
+ prebuiltError = error;
20
+ }
21
+
22
+ if (prebuilt) {
23
+ process.stdout.write(
24
+ `MustardScript: using optional prebuilt addon from ${prebuilt.packageName}\n`,
25
+ );
26
+ process.exit(0);
27
+ }
28
+
29
+ const target = getCurrentPrebuiltTarget();
30
+ if (prebuiltError) {
31
+ process.stdout.write(
32
+ `MustardScript: ignoring invalid optional prebuilt for ${target?.platformArchABI ?? `${process.platform}-${process.arch}`}: ${prebuiltError.message}\n`,
33
+ );
34
+ }
35
+ if (target) {
36
+ process.stdout.write(
37
+ `MustardScript: no installed prebuilt for ${target.platformArchABI}; building from source\n`,
38
+ );
39
+ } else {
40
+ process.stdout.write(
41
+ `MustardScript: no configured prebuilt for ${process.platform}-${process.arch}; building from source\n`,
42
+ );
43
+ }
44
+
45
+ function nativeLibraryExtension(platform) {
46
+ switch (platform) {
47
+ case 'win32':
48
+ return '.dll';
49
+ case 'darwin':
50
+ return '.dylib';
51
+ default:
52
+ return '.so';
53
+ }
54
+ }
55
+
56
+ function parseCargoArtifactPath(output) {
57
+ const expectedExtension = nativeLibraryExtension(process.platform);
58
+ let artifactPath = null;
59
+ for (const line of output.split(/\r?\n/u)) {
60
+ if (line.trim() === '') {
61
+ continue;
62
+ }
63
+ let event;
64
+ try {
65
+ event = JSON.parse(line);
66
+ } catch {
67
+ continue;
68
+ }
69
+ if (event.reason !== 'compiler-artifact') {
70
+ continue;
71
+ }
72
+ if (event.target?.name !== 'mustard_node') {
73
+ continue;
74
+ }
75
+ if (
76
+ !Array.isArray(event.target?.crate_types) ||
77
+ !event.target.crate_types.includes('cdylib')
78
+ ) {
79
+ continue;
80
+ }
81
+ if (!Array.isArray(event.filenames)) {
82
+ continue;
83
+ }
84
+ const filename = event.filenames.find((entry) => entry.endsWith(expectedExtension));
85
+ if (filename) {
86
+ artifactPath = filename;
87
+ }
88
+ }
89
+ if (!artifactPath) {
90
+ throw new Error('MustardScript: cargo did not report a native cdylib artifact');
91
+ }
92
+ return artifactPath;
93
+ }
94
+
95
+ const cargo = process.env.CARGO || 'cargo';
96
+ const cargoOutput = execFileSync(
97
+ cargo,
98
+ [
99
+ 'build',
100
+ '--release',
101
+ '--manifest-path',
102
+ 'crates/mustard-node/Cargo.toml',
103
+ '--message-format',
104
+ 'json-render-diagnostics',
105
+ ],
106
+ {
107
+ cwd: packageRoot,
108
+ encoding: 'utf8',
109
+ stdio: ['ignore', 'pipe', 'inherit'],
110
+ env: process.env,
111
+ },
112
+ );
113
+
114
+ const artifactPath = parseCargoArtifactPath(cargoOutput);
115
+ const outputPath = path.join(packageRoot, getLocalBuildOutputFile());
116
+ fs.copyFileSync(artifactPath, outputPath);
117
+ process.stdout.write(`MustardScript: built local addon at ${path.basename(outputPath)}\n`);
@@ -0,0 +1,124 @@
1
+ 'use strict';
2
+
3
+ const { types } = require('node:util');
4
+
5
+ const { MustardError, callNative } = require('./errors.js');
6
+
7
+ function throwIfAborted(signal) {
8
+ if (signal?.aborted) {
9
+ throw new MustardError('Limit', 'execution cancelled');
10
+ }
11
+ }
12
+
13
+ function getAbortSignal(options, label) {
14
+ if (options === undefined) {
15
+ return undefined;
16
+ }
17
+ if (options === null || typeof options !== 'object') {
18
+ throw new TypeError(`${label} must be an object`);
19
+ }
20
+ const { signal } = options;
21
+ if (signal === undefined) {
22
+ return undefined;
23
+ }
24
+ if (
25
+ typeof signal !== 'object' ||
26
+ signal === null ||
27
+ typeof signal.aborted !== 'boolean' ||
28
+ typeof signal.addEventListener !== 'function' ||
29
+ typeof signal.removeEventListener !== 'function'
30
+ ) {
31
+ throw new TypeError(`${label}.signal must be an AbortSignal`);
32
+ }
33
+ return signal;
34
+ }
35
+
36
+ function withCancellationSignal(native, fn, args, signal) {
37
+ if (signal === undefined) {
38
+ return callNative(fn, ...args);
39
+ }
40
+ const tokenId = callNative(native.createCancellationToken);
41
+ const cancel = () => {
42
+ try {
43
+ callNative(native.cancelCancellationToken, tokenId);
44
+ } catch {
45
+ // Ignore late cancellation after cleanup wins the race.
46
+ }
47
+ };
48
+
49
+ if (signal.aborted) {
50
+ cancel();
51
+ } else {
52
+ signal.addEventListener('abort', cancel, { once: true });
53
+ }
54
+
55
+ try {
56
+ return callNative(fn, ...args, tokenId);
57
+ } finally {
58
+ if (!signal.aborted) {
59
+ signal.removeEventListener('abort', cancel);
60
+ }
61
+ callNative(native.releaseCancellationToken, tokenId);
62
+ }
63
+ }
64
+
65
+ async function settleCapabilityInvocation(capability, args, signal) {
66
+ if (signal?.aborted) {
67
+ return { type: 'cancelled' };
68
+ }
69
+
70
+ let pending;
71
+ try {
72
+ pending = capability(...args);
73
+ } catch (error) {
74
+ return { type: 'error', error };
75
+ }
76
+
77
+ if (!types.isPromise(pending)) {
78
+ return { type: 'value', value: pending };
79
+ }
80
+
81
+ if (signal === undefined) {
82
+ try {
83
+ return {
84
+ type: 'value',
85
+ value: await pending,
86
+ };
87
+ } catch (error) {
88
+ return { type: 'error', error };
89
+ }
90
+ }
91
+
92
+ if (signal.aborted) {
93
+ pending.catch(() => {});
94
+ return { type: 'cancelled' };
95
+ }
96
+
97
+ const ABORTED = Symbol('aborted');
98
+ let onAbort = null;
99
+ const raced = await Promise.race([
100
+ pending.then(
101
+ (value) => ({ type: 'value', value }),
102
+ (error) => ({ type: 'error', error }),
103
+ ),
104
+ new Promise((resolve) => {
105
+ onAbort = () => resolve(ABORTED);
106
+ signal.addEventListener('abort', onAbort, { once: true });
107
+ }),
108
+ ]);
109
+ signal.removeEventListener('abort', onAbort);
110
+
111
+ if (raced === ABORTED) {
112
+ pending.catch(() => {});
113
+ return { type: 'cancelled' };
114
+ }
115
+
116
+ return raced;
117
+ }
118
+
119
+ module.exports = {
120
+ getAbortSignal,
121
+ settleCapabilityInvocation,
122
+ throwIfAborted,
123
+ withCancellationSignal,
124
+ };