{"id":44976614,"url":"https://github.com/rescript-lang/rescript-skip","last_synced_at":"2026-02-18T17:06:13.822Z","repository":{"id":326112018,"uuid":"1103752232","full_name":"rescript-lang/rescript-skip","owner":"rescript-lang","description":"ReScript bindings and examples for Skip Runtime — build reactive services with automatic updates over HTTP/SSE.","archived":false,"fork":false,"pushed_at":"2025-12-08T10:42:04.000Z","size":6788,"stargazers_count":1,"open_issues_count":3,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-12-08T13:00:21.207Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"ReScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/rescript-lang.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-11-25T09:39:02.000Z","updated_at":"2025-11-26T12:08:14.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/rescript-lang/rescript-skip","commit_stats":null,"previous_names":["rescript-lang/rescript-skip"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/rescript-lang/rescript-skip","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rescript-lang%2Frescript-skip","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rescript-lang%2Frescript-skip/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rescript-lang%2Frescript-skip/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rescript-lang%2Frescript-skip/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/rescript-lang","download_url":"https://codeload.github.com/rescript-lang/rescript-skip/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rescript-lang%2Frescript-skip/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29587066,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-18T16:55:40.614Z","status":"ssl_error","status_checked_at":"2026-02-18T16:55:37.558Z","response_time":162,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-02-18T17:06:13.056Z","updated_at":"2026-02-18T17:06:13.811Z","avatar_url":"https://github.com/rescript-lang.png","language":"ReScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Reactivity in Back-ends: A Practical Guide\n\n**Target audience:** Back-end developers curious about reactive systems\n\n## What is reactivity?\n\nThink of reactivity like **spreadsheet formulas for your back-end**. When you update a cell in Excel, all dependent formulas recalculate automatically. Reactive back-ends work the same way: when your data changes, all derived views update automatically and push fresh data to clients—without you manually tracking what needs to refresh.\n\n## Why should you care?\n\nTraditional back-ends require you to manually:\n- Track which data depends on what (dependency graphs)\n- Figure out what to invalidate when something changes\n- Wire up notification logic to tell clients about updates\n- Handle edge cases where clients see stale data\n\n**Reactive back-ends eliminate this boilerplate.** You define your data and relationships once; the runtime handles propagation automatically.\n\n## This Example: A Minimal Reactive Service\n\nThis repo demonstrates SkipRuntime—a reactive engine with ReScript bindings—through a simple working example.\n\n### What it does\n\nWe build a tiny service with:\n- **Input collection:** A key-value store (`input`) that starts with `foo → \"bar\"`\n- **Reactive resource:** An `echo` view that automatically mirrors whatever's in `input`\n- **Two APIs:**\n  - **HTTP** for reading data and making updates\n  - **SSE (Server-Sent Events)** for streaming live updates to clients\n\n### The magic moment\n\n1. Read `echo` → get `{foo: \"bar\"}`\n2. Update `input` → set `foo → \"baz\"` and add `bar → \"qux\"`\n3. Read `echo` again → automatically get `{foo: \"baz\", bar: \"qux\"}`\n4. Subscribe via SSE and watch updates arrive in real-time as they happen (no polling): the runtime pushes the updated entries to you without another request.\n\n**No manual invalidation code. No cache busting. No diffing logic.** The runtime tracks dependencies and pushes updates automatically.\n\n## Running the live demo (LiveClient)\n\n```bash\nnpm install\nnpm run build\nnode examples/LiveClient.res.js\n```\n\n**Expected output:**\n```\nserver: starting wasm service on ports 18080/18081…\nserver: service started\nlive client: initial getAll [ [ 'foo', [ 'bar' ] ] ]\nlive client: after update getAll [ [ 'bar', [ 'qux' ] ], [ 'foo', [ 'baz' ] ] ]\nlive client: subscribing to http://127.0.0.1:18081/v1/streams/...\nlive client: SSE chunk event: init\nid: …\ndata: [[\"bar\",[\"qux\"]],[\"foo\",[\"baz\"]],[\"sse\",[\"ping\"]]]\nserver: service closed\n```\n\nNotice: We never wrote code to update `echo`. It happened automatically when `input` changed.\n\n## How reactivity works here\n\n1. **Define relationships once:** \"Echo mirrors input\"\n2. **Runtime tracks dependencies:** Skip knows `echo` depends on `input`\n3. **Write triggers propagation:** Update `input` → runtime recomputes `echo` → clients get fresh data\n4. **Subscribe for live updates:** Open an SSE stream and receive updates as they happen, no polling needed\n\nThe Skip runtime handles all the plumbing—dependency tracking, incremental recomputation, and streaming. You just declare what depends on what.\n\n## Requirements\n\n- Works on current Node via wasm (no native runtime here; native is Linux-only). Runtime recommends Node \u003e=22.6 \u003c23 for native builds, but the wasm path has worked on newer Node in practice.\n- Two available ports (defaults: 18080 for HTTP, 18081 for SSE).\n- `npm install` to grab Skip packages.\n\n## Tests\n- `npm test` builds and runs the live client (`examples/LiveClient.res.js`) on ports 18080/18081.\n\n## Reactive combinators\n\nSkip’s service graphs are built from composable operators on collections. The most important one in this repo is `reduce`.\n\nConceptually, its type is:\n\n- `reduce : collection\u003ck, v\u003e -\u003e reducer\u003cv, a\u003e -\u003e collection\u003ck, a\u003e`\n\nOn the Skip side (see `bindings/SkipruntimeCore.res`), that's exposed as:\n\n- `EagerCollection.reduce : (~params=?, collection\u003c'k, 'v\u003e, reducer\u003c'v, 'a\u003e) -\u003e collection\u003c'k, 'a\u003e`\n- where a reducer is built as `Reducer.make(~initial, ~add, ~remove=?)`\n\nA reducer is a small state machine:\n\n- `initial(params) : option\u003c'a\u003e` – produce the starting accumulator (or `None` to say “no value yet”)\n- `add(acc, value, params) : 'a` – incorporate a newly-seen value into the accumulator\n- `remove(acc, value, params) : option\u003c'a\u003e` – forget a value; returning `None` tells the engine “I can’t update incrementally for this change, please recompute from scratch for this key.”\n\n`EagerCollection.reduce` maintains one accumulator per key. For each key `k`, the runtime:\n\n- Starts from `initial` (or a recomputed value).\n- When dependencies change, computes the *old* multiset of contributing values and the *new* multiset.\n- Calls `remove` once for each value that used to contribute under `k` (the `old` slice).\n- If all `remove` calls return `Some(acc')`, calls `add` once for each value that now contributes under `k` (the `new` slice).\n- If any `remove` returns `None`, discards the accumulator and recomputes it from scratch via `initial` + `add` over all current values for `k`.\n\nThe contract for `reduce` is:\n\n- **Purity:** `initial`, `add`, and `remove` must be pure and depend only on their arguments.\n- **Correctness under change:** For any key, starting from a valid accumulator for some multiset of values and applying the runtime’s sequence of `remove`/`add` calls (or the full recompute path when `remove` returns `None`) must yield the same result as recomputing from scratch over the current values.\n\nThis contract lets the runtime maintain derived collections incrementally: when inputs change, only affected keys are updated, and for each key the engine updates the stored accumulator using your `remove`/`add` implementation or falls back to a full recompute if you signal that incremental updates are too hard.\n\n## LiveHarness (reducer semantics demo)\n\nAfter LiveClient, `examples/LiveHarness.res` + `LiveHarnessService.*` illustrate how `reduce` works.\n\nThe service exposes:\n\n- **`numbers`**: input collection `string → number`, initially `[a→1, b→2, …, j→10]`.\n- **`doubled`**: each number multiplied by 2 (demonstrates `map`).\n- **`sum`**: total of all numbers under key `\"total\"` (demonstrates `reduce`).\n\n### How the sum works\n\nIn `LiveHarnessService.ts`:\n\n```typescript\nclass SumReducer implements Reducer\u003cnumber, number\u003e {\n  initial = 0;\n  add(acc, value)    { return acc + value; }\n  remove(acc, value) { return acc - value; }\n}\n```\n\nThe `sum` resource is built as `numbers.map(TotalMapper).reduce(SumReducer)`, where `TotalMapper` emits every value under the single key `\"total\"`.\n\n### What happens on update\n\nWhen `numbers[\"c\"]` changes from `3` to `5`:\n\n1. **Mapper runs once** for the changed key `\"c\"`.\n2. **Reducer sees full slices** for key `\"total\"`:\n   - `old` = all 10 previous values `[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]`\n   - `new` = all 10 current values `[1, 2, 5, 4, 5, 6, 7, 8, 9, 10]`\n3. **Engine calls** `remove` 10 times, then `add` 10 times.\n\nThe sum is correct (55 → 57), but the reducer processes O(n) values per update—not just the changed value.\n\n### Why O(n)?\n\nSkip's reactivity is *per collection and per key*. When any upstream key changes, the reducer for `\"total\"` sees the entire old and new contribution lists for that key. There's no finer-grained \"just this value changed\" signal at the reducer level.\n\n### Client-side O(1) alternative\n\nFor truly O(1) aggregates, subscribe to the reactive collection via SSE and maintain the aggregate client-side:\n\n```rescript\n// Subscribe to SSE stream for numbers collection\nlet streamUrl = await Client.getStreamUrl(opts, broker, \"numbers\")\nClientSum.subscribe(streamUrl)\n\n// In ClientSum module: O(1) update when SSE delivers changes\nlet applyUpdate = (key, newValue) =\u003e {\n  let oldValue = state.numbers-\u003eDict.get(key)-\u003eOption.getOr(0.)\n  state.total = state.total -. oldValue +. newValue\n  state.numbers-\u003eDict.set(key, newValue)\n}\n```\n\nThe harness includes a `ClientSum` module that subscribes to `numbers` via SSE. When the server pushes updates, the client applies them in O(1) time—no polling, no re-fetching the whole collection.\n\nRun:\n\n```bash\nnpm run build \u0026\u0026 node examples/LiveHarness.res.js\n```\n\n## What else is in the repo\n\n### Bindings (`bindings/`)\n- **`SkipruntimeCore.res`**: Core types, collections (`EagerCollection`, `LazyCollection`), operators (`map`, `reduce`, `mapReduce`), `Mapper`/`Reducer`/`LazyCompute` factories, notifiers, service instances.\n- **`SkipruntimeHelpers.res`**: HTTP broker (`SkipServiceBroker`), built-in reducers (`Sum`, `Min`, `Max`, `Count`), external service helpers (`PolledExternalService`, `SkipExternalService`), leader-follower topology (`asLeader`, `asFollower`).\n- **`SkipruntimeServer.res`**: `runService` to start HTTP/SSE servers.\n- **`SkipruntimeCoreHelpers.mjs`**: JS helpers for class constructors, enums, and SSE utilities (`subscribeSSE` for streaming).\n\n### Examples (`examples/`)\n- **`LiveClient.res`**: Main demo—starts a service, reads/updates via HTTP, subscribes via SSE.\n- **`LiveHarness.res` + `LiveHarnessService.ts`**: Demonstrates `map` and `reduce` semantics. Includes `ClientSum`, a client-side O(1) accumulator that subscribes to SSE.\n- **`Example.res`**: Binding smoke test—`LoadStatus`, errors, mapper/reducer wiring—without starting the runtime.\n- **`NotifierExample.res`**: Demonstrates notifier callbacks receiving collection updates and watermarks.\n- **`LiveService.ts`**: Minimal service definition for `LiveClient` (echo resource mirroring input).\n\n## The bottom line\n\nReactive back-ends let you **declare what should happen**, not **how to make it happen**. You avoid manually wiring update logic, and clients never see stale data. This example shows the concept end-to-end in ~80 lines of actual service and client code.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frescript-lang%2Frescript-skip","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frescript-lang%2Frescript-skip","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frescript-lang%2Frescript-skip/lists"}