regent 0.14.1
regent: ^0.14.1 copied to clipboard
Optimistic, message-driven state engine: a journal of sealed facts folded into keyed stores and units, with guards, correlation, and merge edges. Pure Dart.
// The COMPLETE tour — every regent capability in one shop:
//
// 1. facts as sealed families (a msg IS a source: its TYPE is its rank)
// 2. stores & units: pure folds, nothing else lives in a memory
// 3. the REGENCY: set order is traversal order; gates protect below
// 4. guards are LAUNCHERS: `.forward` (drop/rewrite/fan-out below) and
// `.mint` (derive a NEW round from index 0 — unjournaled, re-derived
// on replay); the SHADOW LAW: every row folds a sealed GROUP of
// exactly its facts — no wildcard arms anywhere
// 5. `read(catalog)` — judges read the ledger's own state by identity;
// the rows are NAMED by consumer-owned const globals (the audit list)
// 6. an IN-FLIGHT row: request status as honest state, deduped by a gate
// 7. COVERAGE: recorded permission to treat absence as knowledge
// 8. a shadow store + mergeStore: disk cache answers until censored
// 9. a WRITE DOCK: optimism as rows — pending unit, settling gate,
// deadline as a dispatched FACT (timers live in effects)
// 10. unit-from-unit merge: the pending promise answers reads instantly
// 11. events: effects observe (cause, consequence) atomically
// 12. replay: order-(in)dependence as an executable LAW
// 13. a NAMED REGENCY: the wild's commonest crud (load/add/remove list)
// written longhand — sealed family, exhaustive fold, cap gate, one
// spliceable row; dialects are app code, not framework surface
//
// ignore_for_file: avoid_print
import 'package:regent/regent.dart';
// ── 1. Facts. The sealed family is the fold contract; the msg's TYPE says
// what it may claim: wire facts state truth, cached facts fill absence,
// policy facts are minted by gates, timeout facts by effects. ──
sealed class ShopMsg extends Msg {
const ShopMsg();
}
class Product with Identifiable<String> {
const Product(this.id, this.name, this.addedAt);
@override
final String id;
final String name;
final int addedAt; // the page cursor
@override
bool operator ==(Object o) =>
o is Product && o.id == id && o.name == name && o.addedAt == addedAt;
@override
int get hashCode => Object.hash(id, name, addedAt);
}
/// A cursor page of the catalog — the AUTHORITY over its own window.
class CatalogPage extends ShopMsg implements CatalogMsg, InFlightMsg {
const CatalogPage(this.products, {required this.hasMore});
final List<Product> products;
final bool hasMore;
}
/// The disk cache speaking at boot — may only fill ABSENCE, never overwrite.
class CachedCatalog extends ShopMsg implements LocalCatalogMsg {
const CachedCatalog(this.products);
final List<Product> products;
}
/// The request — kept OUT of any reduce family; the in-flight row tracks it.
class LoadCatalog extends ShopMsg implements InFlightMsg {
const LoadCatalog();
}
/// The gate's RULING — MINTED (never dispatched by hand, never journaled;
/// replay re-derives it from the page): the window a page was exhaustive
/// about, and the known ids it thereby declared gone.
class CatalogRuled extends ShopMsg
implements CatalogMsg, LocalCatalogMsg {
const CatalogRuled(this.lo, this.hi, this.gone);
final int? lo, hi;
final Set<String> gone;
}
// ── The SHADOW LAW: each row folds a sealed GROUP of exactly its facts —
// a message joins its family by `extends` and its readers by `implements`,
// so every reduce is exhaustive and a new member is a compile error. ──
sealed class CatalogMsg extends Msg {}
sealed class LocalCatalogMsg extends Msg {}
sealed class InFlightMsg extends Msg {}
sealed class ShopWriteMsg extends Msg {}
/// The optimistic PREDICTION (a rename), its wire echo, and the deadline
/// fact an effect's timer would dispatch — the ledger never holds a Timer.
class RenameShop extends ShopMsg implements ShopWriteMsg {
const RenameShop(this.name);
final String name;
}
class ShopSaved extends ShopMsg implements ShopWriteMsg {
const ShopSaved(this.name);
final String name;
}
class RenameTimedOut extends ShopMsg implements ShopWriteMsg {
const RenameTimedOut();
}
// ── 2. The folds. A memory holds NOTHING but these. ──
/// The catalog table: wire pages upsert; rulings censor; that's all —
/// and the sealed group says EXACTLY that: no wildcard, nothing unheard.
final class Catalog extends Store<String, Product, CatalogMsg> {
const Catalog();
@override
IdentifiableMap<String, Product> reduce(
IdentifiableMap<String, Product> rows, CatalogMsg msg) =>
switch (msg) {
CatalogPage(:final products) => rows.upsertAll(products),
CatalogRuled(:final gone) =>
rows.withoutWhere((id, _) => gone.contains(id)),
};
}
/// 8. The SHADOW: cache fills absence; authority facts only censor. It may
/// hold LESS truth than the wire, never OTHER.
final class LocalCatalog extends Store<String, Product, LocalCatalogMsg> {
const LocalCatalog();
@override
IdentifiableMap<String, Product> reduce(
IdentifiableMap<String, Product> rows, LocalCatalogMsg msg) =>
switch (msg) {
CachedCatalog(:final products) => {
for (final p in products)
if (!rows.containsKey(p.id)) p.id: p,
...rows,
},
CatalogRuled(:final lo, :final hi, :final gone) =>
rows.withoutWhere((id, p) =>
gone.contains(id) ||
((lo == null || p.addedAt >= lo) &&
(hi == null || p.addedAt <= hi))),
};
}
final class LocalSupports extends Projection<Product, String, Product> {
const LocalSupports() : super(catalog, localCatalog);
@override
Product resolve(Product? row, Product local) => row ?? local;
}
/// 7. COVERAGE as a row: which cursor windows the authority has spoken for.
/// A single-fact row may type on the CONCRETE class — trivially exhaustive.
final class CatalogCoverage extends Unit<CoveredRanges<num>, CatalogRuled> {
const CatalogCoverage() : super(const CoveredRanges.none());
@override
CoveredRanges<num> reduce(CoveredRanges<num> covered, CatalogRuled msg) =>
covered.mark(msg.lo, msg.hi);
}
/// 6. IN-FLIGHT as a row: the request folds it in, the page folds it out.
final class CatalogInFlight extends Unit<bool, InFlightMsg> {
const CatalogInFlight() : super(false);
@override
bool reduce(bool state, InFlightMsg msg) => switch (msg) {
LoadCatalog() => true,
CatalogPage() => false,
};
}
/// The shop name — a unit; its optimism lives in the dock BESIDE it. Its
/// group is the ONE fact base folds: the prediction isn't a member at all —
/// base never folds a promise, by TYPE.
final class Shop extends Unit<String, ShopSaved> {
const Shop() : super('corner shop');
@override
String reduce(String name, ShopSaved msg) => msg.name;
}
/// 9. The WRITE DOCK's pending row: the promise as honest state.
final class ShopWrite extends Unit<String?, ShopWriteMsg> {
const ShopWrite() : super(null);
@override
String? reduce(String? pending, ShopWriteMsg msg) => switch (msg) {
RenameShop(:final name) => name,
ShopSaved() || RenameTimedOut() => null,
};
}
/// 10. The dock's merge edge: reads show the promise until it settles.
final class WriteSupportsShop extends UnitProjection<String?, String> {
const WriteSupportsShop() : super(shop, shopWrite);
@override
String resolve(String value, String? pending) => pending ?? value;
}
// ── 4/5. The gates. Judges of the flow: they fold nothing, hold nothing,
// and decide what every row BELOW sees — reading the ledger by identity. ──
/// A duplicate ask is queue noise — dropped before it can reach the wire.
final class DedupeLoad extends Guard<LoadCatalog> {
const DedupeLoad();
@override
Set<Judgment> judge(LoadCatalog msg, ReadStore read) =>
read(catalogInFlight) ? const {} : {.forward(msg)};
}
/// The CACHE gate: a cached product inside a COVERED window is a corpse —
/// the authority already ruled there; strip it before any row folds it.
/// (Without this row the replay law below prints false: the ledger's laws
/// catch a missing regent the moment you state them.)
final class StripCachedCatalog extends Guard<CachedCatalog> {
const StripCachedCatalog();
@override
Set<Judgment> judge(CachedCatalog msg, ReadStore read) {
final covered = read(catalogCoverage);
return {
.forward(CachedCatalog(
[for (final p in msg.products) if (!covered.contains(p.addedAt)) p])),
};
}
}
/// The PAGE gate: resolves what window this page was exhaustive about and
/// MINTS one policy fact — the known ids inside it that went unlisted are
/// GONE (covered absence is knowledge, not silence). The mint is the
/// upward door: it runs as its own round from index 0, unjournaled, and
/// replay re-derives it — a fact the fold already implies, stated as one.
final class RuleCatalogPage extends Guard<CatalogPage> {
const RuleCatalogPage();
@override
Set<Judgment> judge(CatalogPage msg, ReadStore read) {
if (msg.products.isEmpty) return {.forward(msg)};
final cursors = [for (final p in msg.products) p.addedAt]..sort();
final lo = msg.hasMore ? cursors.first : null; // final page: open below
final hi = cursors.last;
final listed = {for (final p in msg.products) p.id};
final known = {
...read(localCatalog),
...read(catalog),
};
return {
.forward(msg),
.mint(CatalogRuled(lo, hi, {
for (final p in known.values)
if (!listed.contains(p.id) &&
(lo == null || p.addedAt >= lo) &&
p.addedAt <= hi)
p.id,
})),
};
}
}
// ── 13. The commonest CRUD in the wild — a load/add/remove list — written
// LONGHAND as a named regency: a sealed family, one exhaustive fold, a cap
// gate, and a Regency subclass. No battery, no framework surface: this
// whole shape is ~35 lines, law-tested like any rows, spliced as ONE row
// of the app graph below. ──
sealed class WishlistMsg extends Msg {
const WishlistMsg();
}
class WishlistLoaded extends WishlistMsg {
const WishlistLoaded(this.items);
final List<Product> items;
}
class WishAdded extends WishlistMsg {
const WishAdded(this.item);
final Product item;
}
class WishRemoved extends WishlistMsg {
const WishRemoved(this.id);
final String id;
}
final class WishlistRows extends Store<String, Product, WishlistMsg> {
const WishlistRows();
@override
IdentifiableMap<String, Product> reduce(
IdentifiableMap<String, Product> rows, WishlistMsg msg) =>
switch (msg) {
WishlistLoaded(:final items) => items.toMapById(),
WishAdded(:final item) => rows.upsert(item),
WishRemoved(:final id) => rows.removeById(id),
};
}
/// The list holds twenty wishes at most — a policy the segment carries.
final class WishlistCap extends Veto<WishAdded> {
const WishlistCap();
@override
bool block(WishAdded msg, ReadStore read) => read(wishlist).length >= 20;
}
/// A named regency is a pure GRAFT: it groups rows for splicing and
/// nothing else — reads stay flat (`read(wishlist)` above never knows the
/// grouping exists).
final class Wishlist extends Regency {
const Wishlist() : super(const {WishlistCap(), wishlist});
}
// ── 3. The REGENCY: the app as a const value — set order IS the queue.
// Gates stand above what they protect; the dock and its unit close the
// set; the projections in `merges` carry their own endpoints. ──
// The rows get NAMES — consumer-owned const globals: `read(catalog)` in a
// judge, `super(catalog, localCatalog)` in a projection (keyword-free —
// const variables are constant expressions), and the regency below reads
// as the app's one audit list.
const catalog = Catalog();
const localCatalog = LocalCatalog();
const catalogCoverage = CatalogCoverage();
const catalogInFlight = CatalogInFlight();
const shop = Shop();
const shopWrite = ShopWrite();
const wishlist = WishlistRows();
const app = Regency({
DedupeLoad(),
catalogInFlight,
StripCachedCatalog(),
RuleCatalogPage(),
catalogCoverage,
localCatalog,
catalog,
shopWrite,
shop,
Wishlist(), // 13. a named regency — its rows splice here
}, merges: {
LocalSupports(),
WriteSupportsShop(),
});
void main() {
// TWO doors: dispatch (write) and at (stand at a position, typed).
final ledger = Ledger.root(app);
final catalogMem = ledger.at(catalog);
final shopMem = ledger.at(shop);
// 11. An effect: observes post-fold, atomically (cause, consequence).
// Taps deliver ASYNC — after the synchronous script below, each seeing a
// consistent cut; an effect's own dispatches enter like any other fact.
catalogMem.events.listen((e) =>
print(' event: ${e.msg.runtimeType} → ${e.after.length} products'));
// Boot: the cache fills absence…
ledger.dispatch(const CachedCatalog(
[Product('p1', 'kettle', 10), Product('p2', 'ghost teapot', 20)]));
// …asks dedupe through the in-flight row…
ledger.dispatch(const LoadCatalog());
ledger.dispatch(const LoadCatalog()); // dropped by the gate
// …and the authority's final page RULES: the unlisted teapot inside its
// window is gone, from the main AND the shadow.
ledger.dispatch(const CatalogPage(
[Product('p1', 'kettle', 10), Product('p3', 'mug', 30)],
hasMore: false));
print('catalog: ${[for (final p in catalogMem.values) p.name]}');
final covered = ledger.at(catalogCoverage).folded;
print('covered 20? ${covered.contains(20)}');
// 9/10. The dock: the promise shows instantly, base never lied, the echo
// settles — and a silent server would settle via a timeout FACT instead.
ledger.dispatch(const RenameShop('corner shop & co'));
print('shop reads "${shopMem.state}", base holds "${shopMem.folded}"');
ledger.dispatch(const ShopSaved('corner shop & co'));
print('settled: "${shopMem.state}"');
// 13. The wishlist regency, spliced rows like any others.
ledger.dispatch(const WishAdded(Product('p3', 'mug', 30)));
ledger.dispatch(const WishRemoved('p3'));
print('wishlist: ${ledger.at(wishlist).ids}');
// 12. The LAW: cache-vs-authority converges in either order.
final a = replay(app, [
const CachedCatalog([Product('p2', 'ghost teapot', 20)]),
const CatalogPage([Product('p3', 'mug', 30)], hasMore: false),
]);
final b = replay(app, [
const CatalogPage([Product('p3', 'mug', 30)], hasMore: false),
const CachedCatalog([Product('p2', 'ghost teapot', 20)]),
]);
print('replay converges: ${_equal(a, b)}');
ledger.close();
}
bool _equal(Map<Object, Object?> a, Map<Object, Object?> b) =>
a.keys.every((r) => '${a[r]}' == '${b[r]}');