Skip to content

Commit

Permalink
v0.0.12
Browse files Browse the repository at this point in the history
  • Loading branch information
JadenSimon committed Dec 9, 2024
1 parent 278bea3 commit e507f2b
Show file tree
Hide file tree
Showing 56 changed files with 3,259 additions and 1,434 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ node_modules
**/.env
**/.env.*
**/.DS_Store
*.d.zig.ts
*.d.zig.ts
internal
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Features:
* Multi-stage programming - run code at build time to create exactly what you need
* Cloud agnostic libraries - write once, deploy anywhere, including locally
* Automatic permissions solver - least privilege permissions via symbol execution
* Native modules - write modules using Zig with automatic TypeScript bindings (coming soon)
* [Native modules](docs/native-modules.md) - write modules using Zig with automatic TypeScript bindings (experimental)
* Everything you need, built-in
* TypeScript compiler and bundler
* Incremental builds (distributed caching coming soon)
Expand Down
119 changes: 119 additions & 0 deletions docs/native-modules.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
## Overview

Native modules are any non-JavaScript code that is callable from within JavaScript. This is sometimes done because:
* Direct access to syscalls is needed
* Code re-use (many libraries are written in C/C++)
* Well-crafted code in a systems language will generally be more performant than JavaScript

Synapse has first-class support for native modules using the [Zig](https://ziglang.org/) programming language. The integration currently requires pinning to a specific Zig version (0.13.0), which is automatically downloaded as-needed.

**`allowArbitraryExtensions` must be enabled in `tsconfig.json` to use native modules.** This requirement may be removed in a future release. Here's a minimal `tsconfig.json` file for reference:
```json
{
"compilerOptions": {
"allowArbitraryExtensions": true
}
}
```

`*.d.zig.ts` files are automatically generated for imported Zig modules. Adding this pattern to `.gitignore` is recommended.

## Basic usage

When working with many primitive types, things "just work". Simply treat Zig files as-if they were any other module, making sure to import them with the `.zig` suffix:

```main.ts
import { add } from './add.zig'

export function main() {
console.log(add(2, 2))
}
```

```add.zig
pub fn add(a: u32, b: u32) u32 {
return a + b;
}
```

## The `js` module

Many things we take for granted in JavaScript (Promises, strings, arrays) do not translate so easily to Zig. The `js` module is shipped with Synapse and provides APIs for working with the JavaScript runtime.


### Strings

Modern JavaScript engines (V8, JSC, etc.) have various optimizations for strings that make it difficult to use them directly from native code. We can simplify things by copying the string into an owned-buffer with `js.UTF8String`:

```fs.zig
const js = @import("js");
const std = @import("std");
pub fn openSync(src: js.UTF8String) !i32 {
const file = try std.fs.openFileAbsolute(src.data, .{});
return .{ file.fd };
}
```

```main.ts
import { openSync } from './fs.zig'

export function main() {
console.log(openSync('fs.zig'))
}
```

Parameters typed as `[:0]u8` are also treated as strings.

### Promises

We can declare an exported function as "async" by returning a `js.Promise`:

```add.zig
const js = @import("js");
pub fn addAsync(a: u32, b: u32) js.Promise(u32) {
return .{ a + b };
}
```

```main.ts
import { addAsync } from './add.zig'

export function main() {
console.log(await add(2, 2))
}
```

Note that `js.Promise` changes how the Zig function is called from JavaScript by running the function in a thread pool. Zig functions calling Zig functions that return `js.Promise` will appear synchronous.

### Errors

Errors returned by a Zig function are bubbled up to the JS side largely as-is:

```error.zig
pub fn fail() !void {
return error.Failed;
}
```

```main.ts
import { fail } from './error.zig'

export function main() {
try {
fail()
} catch (e) {
console.log(e)
}
}
```

This will show `[Error: Native error] { code: 'Failed' }`. Stack traces will be added soon.


### Structs

Structs passed to or returned from native modules are automatically converted to/from JS objects for convenience. This can be opted-out of by using `*js.Object` instead of the struct type.

40 changes: 38 additions & 2 deletions docs/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ describe('a test suite', () => {
})
})
```

Creates a test suite named "a test suite" and a test within that suite called "runs a test", following the BDD-style of naming.

You can also create tests outside of a suite:
Expand All @@ -23,22 +24,57 @@ test('console.log("hi")', () => {
})
```

Note that `it` is an alias for `test`. Both can be used interchangeably.
Note that some functions are aliases and can be used interchangeably:
* `describe` <---> `suite`
* `it` <---> `test`

## Running tests

Use the command `synapse test` to run all tests. You can provide specific filenames to only run tests in those files as well.

Note that tests are considered a "resource" (albeit a local one). That means a "deploy" operation is required to create/update tests. This happens automatically when you run `synapse test`. Any resources that your tests depend on could be updated which might not be wanted. A future release may include ways to control this behavior.

## Test isolation

In contrast to many JS test frameworks, `synapse:test` executes each test in isolation.

For example, these two tests will both pass:
```ts
import { test, expectEqual } from 'synapse:test'

let c = 0

test('one', () => {
expectEqual(++c, 1)
})

test('two', () => {
expectEqual(++c, 1)
})
```

Not everything is isolated between tests. The following are still shared for performance reasons:
* Code not compiled with Synapse, such as from `npm` packages
* The global context i.e `globalThis`

This should not cause problems in the vast majority of cases. If you run into any problems, please create an issue and we can figure out a solution.

## Assertion functions

`synapse:test` exports a few basic functions for making comparisons (equivalents are from `node:assert`):
* `expect` - truthy check, equivalent to `ok`
* `expectEqual` - deep comparison, equivalent to `assertDeepStrictEqual`
* `expectReferenceEqual` - shallow comparison, equivalent to `assertStrictEqual`

One thing that should stand out is the swapping of deep equal and shallow equal. This was done because structural equality is far more useful and common for distributed systems.
Something that should stand out is the swapping of deep equal and shallow equal. This was done because structural equality is far more useful and common for distributed systems.

## Hooks

Per-test lifecycle hooks can be registered using `before` and `after` which run before and after each test, respectively. Nested suites inherit the hooks of their parents.

The order of execution for "inherited" hooks is different between `before` and `after`:
`before` - top-down execution, start with the hooks in the top-most suite and work down
`after`- bottom-up execution, start with the hooks in the bottom-most suite and work up

## Using other testing frameworks

Expand Down
9 changes: 7 additions & 2 deletions examples/sdk-and-cli/sdk/main.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import { HttpService } from 'synapse:srl/compute'
import { Bucket } from 'synapse:srl/storage'
import { fetch } from 'synapse:http'
import { fetch, HttpError } from 'synapse:http'

const bucket = new Bucket()
const service = new HttpService({ auth: 'none' })

const getRoute = service.route('GET', '/{key+}', async req => {
const { key } = req.pathParameters
return bucket.get(key, 'utf-8')
const data = await bucket.get(key, 'utf-8')
if (data === undefined) {
throw new HttpError(`Key not found: ${key}`, { status: 404 })
}

return data
})

const putRoute = service.route('PUT', '/{key+}', async (req, body: string) => {
Expand Down
11 changes: 3 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
{
"name": "synapse",
"version": "0.0.11",
"version": "0.0.12",
"bin": "./src/cli/index.ts",
"dependencies": {
"esbuild": "^0.20.2",
"typescript": "~5.4.5",
"typescript": "~5.5.4",
"postject": "github:Cohesible/postject"
},
"devDependencies": {
Expand All @@ -23,12 +23,7 @@
"synapse": {
"config": {
"exposeInternal": true,
"target": "local",
"zigFiles": [
"src/zig/ast.zig",
"src/zig/fs-ext.zig",
"src/zig/util.zig"
]
"target": "local"
},
"binaryDependencies": {
"node": "https://github.com/Cohesible/node.git",
Expand Down
Loading

0 comments on commit e507f2b

Please sign in to comment.