@abloatai/ablo 0.64.0 → 0.64.2
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/AGENTS.md +6 -0
- package/CHANGELOG.md +70 -0
- package/README.md +6 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/react.d.ts +1 -1
- package/dist/react.d.ts.map +1 -1
- package/dist/react.js +1 -1
- package/dist/react.js.map +1 -1
- package/docs/examples/account-multiplayer.md +3 -0
- package/docs/examples/scoped-agent.md +3 -3
- package/docs/groups.md +144 -25
- package/docs/identity.md +25 -21
- package/docs/index.md +4 -0
- package/docs/integration-guide.md +4 -4
- package/docs/migration.md +95 -2
- package/docs/quickstart.md +12 -5
- package/docs/react.md +59 -16
- package/llms.txt +5 -1
- package/package.json +5 -4
package/AGENTS.md
CHANGED
|
@@ -4,6 +4,12 @@ Ablo lets AI agents and humans safely edit the same typed data without clobberin
|
|
|
4
4
|
|
|
5
5
|
Claims don't lock. If another writer holds the row, `claim` waits for them and re-reads the fresh row before handing it to you — so two writers serialize instead of clobbering.
|
|
6
6
|
|
|
7
|
+
Groups structure which shared state reaches each person or agent. Declare the
|
|
8
|
+
data's scope and participant authority, then let authorized subscriptions keep
|
|
9
|
+
reactive views current; HTTP agents explicitly read or observe changes. Start
|
|
10
|
+
with [Groups and shared context](./docs/groups.md) to connect membership, access,
|
|
11
|
+
loading and updates.
|
|
12
|
+
|
|
7
13
|
## Start here — scaffold with `ablo init`
|
|
8
14
|
|
|
9
15
|
Before choosing among identifier claims, row claims, captured reads, atomic
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,75 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.64.2
|
|
4
|
+
|
|
5
|
+
### More complete upgrade guidance
|
|
6
|
+
|
|
7
|
+
`ablo upgrade` now identifies removed `SyncProvider` imports and the removed
|
|
8
|
+
`AbloProvider` `userId` prop, including aliased imports. Its guidance points to
|
|
9
|
+
the current provider and authenticated client session. React examples and
|
|
10
|
+
identity documentation now use the supported props consistently.
|
|
11
|
+
|
|
12
|
+
### Registration examples match the scaffold
|
|
13
|
+
|
|
14
|
+
The quickstart now includes the empty type import from `@abloatai/ablo` that
|
|
15
|
+
`ablo init` already generates. It loads the SDK declaration before augmenting
|
|
16
|
+
`Register`, so applications copying the example retain the existing module's
|
|
17
|
+
types. The groups guide also explains how shared context relates to membership
|
|
18
|
+
and subscription changes.
|
|
19
|
+
|
|
20
|
+
This patch introduces no new API changes.
|
|
21
|
+
|
|
22
|
+
## 0.64.1
|
|
23
|
+
|
|
24
|
+
### A schema that survives the journey into a package
|
|
25
|
+
|
|
26
|
+
Sharing a schema through a compiled package could leave an application facing
|
|
27
|
+
pages of empty-object and unknown-type errors. The schema was present, but its
|
|
28
|
+
registration no longer reached the parts of Ablo that needed it. Registration
|
|
29
|
+
now carries through the published declarations into Transaction and Humans.
|
|
30
|
+
When it is missing, schema-free mutators report what the application needs to
|
|
31
|
+
supply.
|
|
32
|
+
|
|
33
|
+
Passing the schema explicitly is the recommended approach for mutators. The
|
|
34
|
+
integration guide also explains how to share a schema and React bindings across
|
|
35
|
+
packages, so a monorepo's source imports and its consumers' compiled imports
|
|
36
|
+
agree about the same application.
|
|
37
|
+
|
|
38
|
+
### React makes the difference between reading and writing visible
|
|
39
|
+
|
|
40
|
+
A component reading a snapshot and an event handler issuing a write now ask for
|
|
41
|
+
different things. `useAblo(selector)` returns the render snapshot;
|
|
42
|
+
`useAbloClient()` returns the client used for operations. The zero-argument
|
|
43
|
+
`useAblo()` call is removed, making that choice visible at the call site.
|
|
44
|
+
|
|
45
|
+
Mutation failures have a dedicated hook again. `useMutationFailure` manages the
|
|
46
|
+
subscription and its cleanup, removing the effect each application otherwise
|
|
47
|
+
had to maintain. Framework adapters can obtain the supported store contract
|
|
48
|
+
through `getAbloStore(client)` from `@abloatai/ablo/client`.
|
|
49
|
+
|
|
50
|
+
### Presence can leave the current session out
|
|
51
|
+
|
|
52
|
+
A view of who else is working on a record can now request
|
|
53
|
+
`{ excludeSelf: true }` through `usePresence` or `presence.forModel`. The filter
|
|
54
|
+
removes the current session; another tab belonging to the same person remains
|
|
55
|
+
visible. Applications no longer need to repeat that distinction themselves.
|
|
56
|
+
|
|
57
|
+
Generic collaboration subscriptions also retain the event arguments declared
|
|
58
|
+
by their adapters. Handlers that need authenticated sender context use
|
|
59
|
+
`collaboration.subscribe` on the session or `subscribeCollaboration` on the
|
|
60
|
+
transport.
|
|
61
|
+
|
|
62
|
+
### One release, with an explicit upgrade path
|
|
63
|
+
|
|
64
|
+
Ablo, Transaction, Humans and the CLI now declare exact compatibility
|
|
65
|
+
requirements, keeping their versions together. Provider terminology is
|
|
66
|
+
consistently `AbloProvider`, including the context used by framework adapters.
|
|
67
|
+
|
|
68
|
+
Despite the patch number, 0.64.1 includes API removals. `ablo upgrade` reports
|
|
69
|
+
migration hints, and the
|
|
70
|
+
[upgrade guide](https://github.com/Abloatai/ablo/blob/v0.64.1/packages/ablo/docs/migration.md)
|
|
71
|
+
covers the hook replacements, provider context, presence fields and claims.
|
|
72
|
+
|
|
3
73
|
## 0.64.0
|
|
4
74
|
|
|
5
75
|
React reads the same core objects as ordinary Ablo code. Read connection state with `ablo.status` outside React and `useAblo(ablo => ablo.status)` inside React; its type is `Ablo.Status`. Presence reads through `useAblo(ablo => ablo.presence.others)` or `ablo.presence.forModel(model, id)` now react to activity changes. The unregistered-schema selector also preserves core properties and methods.
|
package/README.md
CHANGED
|
@@ -29,6 +29,12 @@ Every write goes through it, so authority, idempotency, conflicts, ordering,
|
|
|
29
29
|
and confirmation are enforced in one place. Your Postgres remains the source of
|
|
30
30
|
truth.
|
|
31
31
|
|
|
32
|
+
Groups structure which shared state reaches each person or agent. Declare the
|
|
33
|
+
data's scope and participant authority, then let authorized subscriptions keep
|
|
34
|
+
reactive views current; HTTP agents explicitly read or observe changes. Start
|
|
35
|
+
with [Groups and shared context](./docs/groups.md) to connect membership, access,
|
|
36
|
+
loading and updates.
|
|
37
|
+
|
|
32
38
|
## Why Ablo
|
|
33
39
|
|
|
34
40
|
Software used to have one writer: a human clicking through an application. AI
|
package/dist/index.d.ts
CHANGED
|
@@ -8,4 +8,12 @@ export { Ablo as default } from '@abloatai/transaction';
|
|
|
8
8
|
*/
|
|
9
9
|
export { noopLogger } from '@abloatai/transaction/logger';
|
|
10
10
|
export type { Logger } from '@abloatai/transaction/logger';
|
|
11
|
+
/** Application registration, owned here so augmentation survives re-exports. */
|
|
12
|
+
export interface Register {
|
|
13
|
+
}
|
|
14
|
+
type PublicRegister = Register;
|
|
15
|
+
declare module '@abloatai/transaction/types/global' {
|
|
16
|
+
interface Register extends PublicRegister {
|
|
17
|
+
}
|
|
18
|
+
}
|
|
11
19
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,uBAAuB,CAAC;AACtC,OAAO,EAAE,IAAI,IAAI,OAAO,EAAE,MAAM,uBAAuB,CAAC;AAExD;;;;;GAKG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,8BAA8B,CAAC;AAC1D,YAAY,EAAE,MAAM,EAAE,MAAM,8BAA8B,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,uBAAuB,CAAC;AACtC,OAAO,EAAE,IAAI,IAAI,OAAO,EAAE,MAAM,uBAAuB,CAAC;AAExD;;;;;GAKG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,8BAA8B,CAAC;AAC1D,YAAY,EAAE,MAAM,EAAE,MAAM,8BAA8B,CAAC;AAE3D,gFAAgF;AAEhF,MAAM,WAAW,QAAQ;CAAG;AAC5B,KAAK,cAAc,GAAG,QAAQ,CAAC;AAI/B,OAAO,QAAQ,oCAAoC,CAAC;IAClD,UAAU,QAAS,SAAQ,cAAc;KAAG;CAC7C"}
|
package/dist/react.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
/** Core client and the explicitly supported React bindings. */
|
|
2
2
|
export { Ablo, humans } from '@abloatai/humans';
|
|
3
|
-
export { AbloProvider, createAbloReact, useAblo, usePresence, useMutators, useUndoScope, } from '@abloatai/humans/react';
|
|
3
|
+
export { AbloProvider, createAbloReact, useAblo, useAbloClient, useMutationFailure, usePresence, useMutators, useUndoScope, } from '@abloatai/humans/react';
|
|
4
4
|
//# sourceMappingURL=react.d.ts.map
|
package/dist/react.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"react.d.ts","sourceRoot":"","sources":["../src/react.ts"],"names":[],"mappings":"AAAA,+DAA+D;AAC/D,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAChD,OAAO,EACL,YAAY,EACZ,eAAe,EACf,OAAO,EACP,WAAW,EACX,WAAW,EACX,YAAY,GACb,MAAM,wBAAwB,CAAC"}
|
|
1
|
+
{"version":3,"file":"react.d.ts","sourceRoot":"","sources":["../src/react.ts"],"names":[],"mappings":"AAAA,+DAA+D;AAC/D,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAChD,OAAO,EACL,YAAY,EACZ,eAAe,EACf,OAAO,EACP,aAAa,EACb,kBAAkB,EAClB,WAAW,EACX,WAAW,EACX,YAAY,GACb,MAAM,wBAAwB,CAAC"}
|
package/dist/react.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
/** Core client and the explicitly supported React bindings. */
|
|
2
2
|
export { Ablo, humans } from '@abloatai/humans';
|
|
3
|
-
export { AbloProvider, createAbloReact, useAblo, usePresence, useMutators, useUndoScope, } from '@abloatai/humans/react';
|
|
3
|
+
export { AbloProvider, createAbloReact, useAblo, useAbloClient, useMutationFailure, usePresence, useMutators, useUndoScope, } from '@abloatai/humans/react';
|
|
4
4
|
//# sourceMappingURL=react.js.map
|
package/dist/react.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"react.js","sourceRoot":"","sources":["../src/react.ts"],"names":[],"mappings":"AAAA,+DAA+D;AAC/D,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAChD,OAAO,EACL,YAAY,EACZ,eAAe,EACf,OAAO,EACP,WAAW,EACX,WAAW,EACX,YAAY,GACb,MAAM,wBAAwB,CAAC"}
|
|
1
|
+
{"version":3,"file":"react.js","sourceRoot":"","sources":["../src/react.ts"],"names":[],"mappings":"AAAA,+DAA+D;AAC/D,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAChD,OAAO,EACL,YAAY,EACZ,eAAe,EACf,OAAO,EACP,aAAa,EACb,kBAAkB,EAClB,WAAW,EACX,WAAW,EACX,YAAY,GACb,MAAM,wBAAwB,CAAC"}
|
|
@@ -7,6 +7,9 @@ The maintained reference lives in
|
|
|
7
7
|
Start at `src/index.ts`, then follow its owned `accounts`, `agent` and `workspace`
|
|
8
8
|
boundaries. Its README contains the install, isolated-branch setup and test commands.
|
|
9
9
|
|
|
10
|
+
For the full participant lifecycle and its current cache behavior, see
|
|
11
|
+
[Groups and shared context](../groups.md).
|
|
12
|
+
|
|
10
13
|
## One account, one authorization rule
|
|
11
14
|
|
|
12
15
|
“People in this account can see its chats” requires both a row rule and a
|
|
@@ -104,7 +104,7 @@ const ablo = Ablo({
|
|
|
104
104
|
});
|
|
105
105
|
|
|
106
106
|
// The agent run is mounted on behalf of its triggering user.
|
|
107
|
-
<AbloProvider client={ablo}
|
|
107
|
+
<AbloProvider client={ablo}>
|
|
108
108
|
{children}
|
|
109
109
|
</AbloProvider>
|
|
110
110
|
```
|
|
@@ -115,11 +115,11 @@ the agent can never reach a workspace its triggering user couldn't.
|
|
|
115
115
|
|
|
116
116
|
## 3. Write: it fans out to everyone on that workspace
|
|
117
117
|
|
|
118
|
-
Inside any component under the provider, grab the scoped client with `
|
|
118
|
+
Inside any component under the provider, grab the scoped client with `useAbloClient()`
|
|
119
119
|
and write. The connection is already narrowed to `workspace:<workspaceId>` from Step 2.
|
|
120
120
|
|
|
121
121
|
```ts
|
|
122
|
-
const ablo =
|
|
122
|
+
const ablo = useAbloClient();
|
|
123
123
|
|
|
124
124
|
// Other participants subscribed to workspace:<workspaceId> — a reviewer agent, a
|
|
125
125
|
// person watching in the UI — receive this delta in realtime. Participants on
|
package/docs/groups.md
CHANGED
|
@@ -1,16 +1,137 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Groups and shared context
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> Structure which shared state reaches each participant, and how their view stays current.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
> convention this rests on.
|
|
5
|
+
Groups connect the structure of your data to the people and agents who receive
|
|
6
|
+
it. A group names a shared context, such as `account:acme` or `workspace:abc`.
|
|
7
|
+
Membership and authorization determine the eligible view; subscriptions and
|
|
8
|
+
client loading determine how that view reaches a participant.
|
|
10
9
|
|
|
11
|
-
|
|
10
|
+
Start here to understand **why this participant receives this record**. Use
|
|
11
|
+
[Identity](./identity.md) for authentication and credential issuance, and the
|
|
12
|
+
[account multiplayer walkthrough](./examples/account-multiplayer.md) for the
|
|
13
|
+
maintained application that puts these pieces together.
|
|
14
|
+
|
|
15
|
+
## One context, several decisions
|
|
16
|
+
|
|
17
|
+
Ablo has existing declarations for these decisions; there is no single group
|
|
18
|
+
object that configures all of them.
|
|
19
|
+
|
|
20
|
+
| Decision | Existing declaration or behavior |
|
|
21
|
+
| --- | --- |
|
|
22
|
+
| Which records form a context? | Model `groups.root` creates a group per root record; children inherit through `belongsTo` relationships marked `parent: true`. Ordinary references do not propagate membership. `groups.roles` supplies explicit field-based routes. |
|
|
23
|
+
| Who belongs? | Schema `groups.grants` declares a membership edge through its `subject` and `scope` relations within an organization. A trusted backend can also issue session `groups` after verifying application membership. |
|
|
24
|
+
| Which rows may they access? | Model `policy` establishes the read/tenant boundary; `subject` requires a matching credential group for the named row field. Delivery routing alone is not a read policy. |
|
|
25
|
+
| What may they do? | Session `can` grants model operations. Read membership does not grant update or claim authority. |
|
|
26
|
+
| Which changes reach them? | Server-authorized subscriptions match the row's delivery groups. Requested groups cannot widen credential authority. |
|
|
27
|
+
| What is local? | Reactive clients bootstrap and maintain local state; HTTP clients explicitly fetch data. Client loading is distinct from permission to read. |
|
|
28
|
+
|
|
29
|
+
For a model with `subject`, its subject group is the **exclusive delivery
|
|
30
|
+
route**. Parent groups, explicit roles and additional routes cannot provide an
|
|
31
|
+
alternate path to that row. For other routed models, delivery matches any
|
|
32
|
+
eligible group; declaring a narrow route does not narrow an otherwise broad
|
|
33
|
+
read policy. `groups.routingOnly: true` acknowledges that deliberate difference,
|
|
34
|
+
not an authorization grant.
|
|
35
|
+
|
|
36
|
+
## Follow one conversation
|
|
37
|
+
|
|
38
|
+
The account multiplayer reference declares this model:
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
import { defineSchema, model, z } from '@abloatai/ablo/schema';
|
|
42
|
+
|
|
43
|
+
const schema = defineSchema({
|
|
44
|
+
conversations: model({
|
|
45
|
+
accountId: z.string().min(1),
|
|
46
|
+
title: z.string(),
|
|
47
|
+
executionOwner: z.string().nullable(),
|
|
48
|
+
executionState: z.enum(['idle', 'generating']),
|
|
49
|
+
}, { subject: { field: 'accountId', group: 'account' } }),
|
|
50
|
+
});
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
A conversation whose `accountId` is `acme` requires `account:acme`. The
|
|
54
|
+
application verifies Alice's membership before issuing her browser session with
|
|
55
|
+
that group and `can: { conversations: ['read'] }`. The agent gets the same group
|
|
56
|
+
with `read` and `update` authority. The reference verifies account membership in
|
|
57
|
+
application code; it does not use a schema `groups.grants` membership model.
|
|
58
|
+
|
|
59
|
+
Both can read the conversation. Alice's browser cannot update it with its
|
|
60
|
+
read-only credential: the reference performs human writes through a separately
|
|
61
|
+
scoped server client. The agent may update it, subject to the write's claims and
|
|
62
|
+
read checks. An outsider cannot gain access by supplying `accountId: 'acme'` in
|
|
63
|
+
a filter or by requesting an unauthorized subscription.
|
|
64
|
+
|
|
65
|
+
```mermaid
|
|
66
|
+
flowchart TD
|
|
67
|
+
R["Conversation: accountId = acme"] --> S["Subject: account:acme"]
|
|
68
|
+
S --> G["Trusted groups + operation grants"]
|
|
69
|
+
G -->|Bootstrap and live updates| H["Alice's reactive local view"]
|
|
70
|
+
G -->|Explicit reads or log requests| A["Agent's working context"]
|
|
71
|
+
```
|
|
12
72
|
|
|
13
|
-
|
|
73
|
+
The diagram describes data flow. Group membership does not prove that a
|
|
74
|
+
participant is connected, has loaded every record, or has acted on an update.
|
|
75
|
+
Presence describes activity; it does not grant authority or locate cached bytes.
|
|
76
|
+
|
|
77
|
+
## A participant's lifecycle
|
|
78
|
+
|
|
79
|
+
| Event | What happens today |
|
|
80
|
+
| --- | --- |
|
|
81
|
+
| Join | The backend authenticates the participant and verifies membership before minting a scoped session. Issuance does not itself load records. |
|
|
82
|
+
| Load | Alice's reactive client loads its authorized baseline and consumes updates. An HTTP agent calls model reads/lists or observes the ordered log; it has no reactive local graph. |
|
|
83
|
+
| Change | A confirmed conversation change routes through `account:acme` to eligible subscribers. An HTTP agent must explicitly read again or consume log changes to update its working context. |
|
|
84
|
+
| Gain a group | On the incremental group-added path, the reactive client records membership and receives covering deltas for newly visible rows. The full-diff path instead requests re-bootstrap. |
|
|
85
|
+
| Reconnect | The reactive client compares current server-issued groups with stored subscription metadata. Detected shrinkage clears local storage and memory and marks a full bootstrap as required; otherwise normal catch-up applies. |
|
|
86
|
+
| Lose a group | On a group-removal notification, the reactive client clears its managed database and object pool, updates subscription metadata and requests re-bootstrap. It does not selectively evict that group's rows. |
|
|
87
|
+
| Switch account | The reference disposes the previous account client and creates a client using the newly authorized account endpoint. |
|
|
88
|
+
|
|
89
|
+
Group-change handling is a runtime path, not a promise that every change in an
|
|
90
|
+
external membership database immediately invalidates every issued credential.
|
|
91
|
+
The application must connect its membership and credential lifecycle to Ablo.
|
|
92
|
+
An offline participant cannot process a revocation notification until it
|
|
93
|
+
reconnects; managed-cache clearing cannot retract copies retained by application
|
|
94
|
+
code or an agent. Clients configured without automatic bootstrap do not fetch
|
|
95
|
+
a full baseline after a group-change notification; they rely on covering deltas or
|
|
96
|
+
explicit reads for data.
|
|
97
|
+
|
|
98
|
+
Consider a participant authorized for both `account:acme` and `account:beta`.
|
|
99
|
+
Losing Acme currently clears the client's whole managed cache, including cached
|
|
100
|
+
Beta records, before rebuilding the remaining authorized view. Beta records
|
|
101
|
+
remain eligible for loading. For non-subject routing where one row belongs to
|
|
102
|
+
several groups, losing one matching group likewise does not alone establish
|
|
103
|
+
that the row is inaccessible; remaining authorization and routes matter.
|
|
104
|
+
|
|
105
|
+
## Understand the living system
|
|
106
|
+
|
|
107
|
+
Inspect a participant through three separate questions: **what may they see,
|
|
108
|
+
what are they subscribed to, and what have they loaded?** To explain an individual
|
|
109
|
+
record, follow its model's subject or routing declaration, the participant's
|
|
110
|
+
trusted groups and operation grants, then its client transport and lifecycle.
|
|
111
|
+
|
|
112
|
+
These distinctions also help assess a group design:
|
|
113
|
+
|
|
114
|
+
| Symptom | Design question |
|
|
115
|
+
| --- | --- |
|
|
116
|
+
| Many irrelevant updates | Is the delivery group broader than the participant's work? |
|
|
117
|
+
| One task needs many groups | Has the shared context been fragmented too far? |
|
|
118
|
+
| One change reaches many subscribers | Is that fan-out useful, and do all subscribers need live delivery? |
|
|
119
|
+
| Frequent group-premise rejection | Does the decision depend on the whole group, or only particular rows/fields? |
|
|
120
|
+
| Slow loading or catch-up | How much authorized state is being materialized, and how much changed while offline? |
|
|
121
|
+
|
|
122
|
+
These are evaluation questions, not a built-in group score or per-participant
|
|
123
|
+
cache dashboard. A group does not configure blob prefetch, cache placement or
|
|
124
|
+
selective eviction. Those would be additional capabilities built on these scope
|
|
125
|
+
and update signals.
|
|
126
|
+
|
|
127
|
+
## Changes and decisions
|
|
128
|
+
|
|
129
|
+
Receiving an update keeps a live view current. Declaring a read premise checks
|
|
130
|
+
whether a particular decision is still valid when written. A group can serve
|
|
131
|
+
both purposes, but membership alone neither locks records nor makes them
|
|
132
|
+
mutually consistent.
|
|
133
|
+
|
|
134
|
+
### Protect a decision based on a group
|
|
14
135
|
|
|
15
136
|
An agent reads workspace `A` to write document `B`. A moment later it reads `B` to write
|
|
16
137
|
block `C`. Between those steps someone else edits `A`. The agent is now building
|
|
@@ -74,18 +195,17 @@ does not retain the row contents as read evidence.
|
|
|
74
195
|
for you and leaves the third to you — on purpose.
|
|
75
196
|
|
|
76
197
|
**Routing — who hears about a change.** Every row belongs to one or more sync
|
|
77
|
-
groups, and a write fans out to
|
|
78
|
-
|
|
79
|
-
|
|
198
|
+
groups, and a write fans out to its delivery groups. For a model without an
|
|
199
|
+
exclusive `subject` route, declared scope roots and
|
|
200
|
+
relationships can route a block change to `block:…`, `document:…`, and
|
|
201
|
+
`workspace:…`, so authorized workspace subscribers receive it. This is delivery,
|
|
80
202
|
resolved by walking the ownership tree at commit time. It routes the change; it
|
|
81
203
|
never recomputes a value.
|
|
82
204
|
|
|
83
|
-
**Structural cascade — what disappears with a change.**
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
the subtree and emits a tombstone for each descendant, routed to the right
|
|
88
|
-
group. Watchers see the whole subtree vanish.
|
|
205
|
+
**Structural cascade — what disappears with a change.** A declared ownership
|
|
206
|
+
relationship can make deleting a parent remove its descendants. Clients need
|
|
207
|
+
routed deletion deltas to remove those records from their views. This follows
|
|
208
|
+
the relationship and delete path; sharing a group alone does not cascade deletes.
|
|
89
209
|
|
|
90
210
|
**Value recomputation — what a change implies for derived state.** If `B` holds a
|
|
91
211
|
number rolled up from `A`, the engine does not recompute `B` when `A` changes. It
|
|
@@ -116,8 +236,8 @@ decides what that means for its own state, commits, and *its* commit is what
|
|
|
116
236
|
reaches `C`.
|
|
117
237
|
|
|
118
238
|
The direction matters. The signal flows forward, A to B to C, and each hop is a
|
|
119
|
-
real write an actor chose to make.
|
|
120
|
-
|
|
239
|
+
real write an actor chose to make. Group routing supplies the delivery edges, and declared read premises add
|
|
240
|
+
stale-work checks to writes; the actors are
|
|
121
241
|
the runtime that walks them. It is closer to a dataset an analyst
|
|
122
242
|
recalculates cell by cell than to a reactive engine that recomputes the whole
|
|
123
243
|
column for you.
|
|
@@ -167,11 +287,10 @@ group premise fires when *anything* in the group moves — so a group that is to
|
|
|
167
287
|
broad wakes actors for changes they don't care about, and one that is too narrow
|
|
168
288
|
misses the dependency you meant to track.
|
|
169
289
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
them coarse everywhere else.
|
|
290
|
+
Choose groups around shared work and authorized audiences. Use a group premise
|
|
291
|
+
when a decision depends on that whole context; use row or field premises when
|
|
292
|
+
it depends on less. Overlapping routing groups can express useful audiences,
|
|
293
|
+
but do not create transaction boundaries or a consistency guarantee.
|
|
175
294
|
|
|
176
295
|
---
|
|
177
296
|
|
package/docs/identity.md
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
|
-
# Identity
|
|
1
|
+
# Identity and credentials
|
|
2
2
|
|
|
3
3
|
> Who is connecting, and which slice of state they are allowed to see.
|
|
4
4
|
|
|
5
|
+
Start with [Groups and shared context](./groups.md) for how data membership,
|
|
6
|
+
authorization, subscriptions and local state fit together. This guide owns
|
|
7
|
+
authentication, credential issuance and the schema wiring behind that view.
|
|
8
|
+
|
|
5
9
|
This is the doc the Quickstart skips: **who is connecting, and which slice
|
|
6
10
|
of shared state do they get?** If you've wired `<AbloProvider client={ablo}>`
|
|
7
11
|
and wondered where org / team / user actually come from — start here.
|
|
@@ -15,7 +19,7 @@ Ablo is not an identity provider. It has no login, no password store, no
|
|
|
15
19
|
session of its own. You keep whatever you already use — Clerk, Auth0,
|
|
16
20
|
NextAuth, WorkOS, your own session table. Ablo's job begins **after** you've
|
|
17
21
|
authenticated the user: you hand Ablo the already-authenticated identity, and
|
|
18
|
-
Ablo
|
|
22
|
+
Ablo enforces that credential's groups, model read policies and operation grants.
|
|
19
23
|
|
|
20
24
|
## Inspect the credential the application is actually using
|
|
21
25
|
|
|
@@ -75,13 +79,12 @@ that.
|
|
|
75
79
|
|
|
76
80
|
## What a sync group is
|
|
77
81
|
|
|
78
|
-
A **sync group**
|
|
79
|
-
`
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
row's sync group is in their allowed set.
|
|
82
|
+
A **sync group** names shared state, such as `org:acme` or `workspace:abc123`.
|
|
83
|
+
The server checks allowed groups for delivery; model `policy` and `subject`
|
|
84
|
+
rules govern row access, and capability operations govern permitted actions.
|
|
85
|
+
Routing a row to a group does not itself authorize an HTTP read or write.
|
|
86
|
+
See [the group lifecycle](./groups.md#a-participants-lifecycle) for loading,
|
|
87
|
+
updates, reconnects and removal.
|
|
85
88
|
|
|
86
89
|
There is no built-in `org` / `team` / `user` concept in the engine. Those are
|
|
87
90
|
*your* domain words. Ablo only knows sync-group strings. The mapping from "this
|
|
@@ -133,7 +136,7 @@ export const schema = defineSchema(
|
|
|
133
136
|
// 2. app/providers.tsx — a HUMAN gets their full org / team scope.
|
|
134
137
|
// teamIds is set on the client you build (Ablo({ schema, teamIds: user.teamIds })),
|
|
135
138
|
// not passed to the provider; the provider just takes that client.
|
|
136
|
-
<AbloProvider client={ablo}
|
|
139
|
+
<AbloProvider client={ablo}>
|
|
137
140
|
{children}
|
|
138
141
|
</AbloProvider>
|
|
139
142
|
```
|
|
@@ -255,9 +258,11 @@ Delivery scoping is two declarations that meet in the middle. One describes the
|
|
|
255
258
|
changes when the row's sync groups intersect the participant's allowed set.
|
|
256
259
|
|
|
257
260
|
That intersection does not itself authorize an HTTP read. A model's `policy`
|
|
258
|
-
governs read access
|
|
259
|
-
|
|
260
|
-
|
|
261
|
+
governs read access, while `subject` can require a credential group for a row
|
|
262
|
+
field. Operation grants bound the actions. A subject-scoped row uses only its
|
|
263
|
+
subject delivery group; other routes cannot bypass that boundary. See
|
|
264
|
+
[Groups and shared context](./groups.md#one-context-several-decisions) for how
|
|
265
|
+
these declarations fit together.
|
|
261
266
|
|
|
262
267
|
### Half 1 (`identityRoles`): identity → allowed groups
|
|
263
268
|
|
|
@@ -408,8 +413,7 @@ server, never by the browser.**
|
|
|
408
413
|
|
|
409
414
|
## Wiring the provider
|
|
410
415
|
|
|
411
|
-
The identity your server resolved is carried by the client
|
|
412
|
-
`userId` prop. In a Next.js app, resolve the user in a Server Component and pass
|
|
416
|
+
The identity your server resolved is carried by the authenticated client session. In a Next.js app, resolve the user in a Server Component and pass
|
|
413
417
|
it down. Build the client once (the schema, `teamIds`, and the `apiKey` resolver
|
|
414
418
|
live here; entity narrowing rides the minted session's `groups`), then hand
|
|
415
419
|
it to the provider:
|
|
@@ -450,7 +454,7 @@ export function Providers({
|
|
|
450
454
|
}) {
|
|
451
455
|
const ablo = useMemo(() => makeAblo(user), [user.id]);
|
|
452
456
|
return (
|
|
453
|
-
<AbloProvider client={ablo}
|
|
457
|
+
<AbloProvider client={ablo} fallback={<AppSkeleton />}>
|
|
454
458
|
{children}
|
|
455
459
|
</AbloProvider>
|
|
456
460
|
);
|
|
@@ -461,11 +465,11 @@ What carries identity — and just as importantly, what does *not* set the bound
|
|
|
461
465
|
|
|
462
466
|
| Where | Purpose |
|
|
463
467
|
| ------------ | ------------------------------------------------------------------------------------------------ |
|
|
464
|
-
|
|
|
468
|
+
| Application authentication context | Supplies identity for app-owned fields and UI. The provider has no `userId` prop; the server enforces scope from the authenticated session. |
|
|
465
469
|
| `teamIds` (on the client) | Team ids expanded into team sync groups via your `identityRoles`. |
|
|
466
470
|
| `groups` (at session mint) | Optional. **Narrows** a minted session's subscription to a subset of what auth already allows: it can never widen it. Passed to `sessions.create({ user \| agent, groups })`; build entries with `syncGroup(kind, id)`. Use it to scope an agent (or a focused page's session) to one entity, e.g. `[syncGroup('workspace', 'abc123')]`. |
|
|
467
471
|
|
|
468
|
-
Because the server is the boundary, a client that changes
|
|
472
|
+
Because the server is the boundary, a client that changes application identity state to another
|
|
469
473
|
user's id does not gain their data — the server resolves and enforces the real
|
|
470
474
|
identity on the connection. These are how your app *tells* Ablo who it
|
|
471
475
|
already authenticated, not how it *proves* it.
|
|
@@ -644,9 +648,9 @@ The best practices Ablo inherits from that lineage:
|
|
|
644
648
|
the line precisely: [token parameters are trusted and usable for access
|
|
645
649
|
control; client parameters are not](https://docs.powersync.com/usage/sync-rules/advanced-topics/client-parameters).
|
|
646
650
|
In Ablo terms, the identity your server vouches for — and the session's
|
|
647
|
-
`groups`, minted server-side — are the *trusted* claims that set scope;
|
|
648
|
-
|
|
649
|
-
never the boundary.
|
|
651
|
+
`groups`, minted server-side — are the *trusted* claims that set scope; application
|
|
652
|
+
identity state is *untrusted client input* — convenient for app-owned fields, but
|
|
653
|
+
never the boundary. Changing that state in the browser grants nothing.
|
|
650
654
|
|
|
651
655
|
3. **Scope by a hierarchical naming convention, declared once.** Ablo's `kind:id`
|
|
652
656
|
group naming (`org:…` / `team:…` from `identityRoles`, `workspace:…` from a model's
|
package/docs/index.md
CHANGED
|
@@ -27,6 +27,10 @@ interfaces.
|
|
|
27
27
|
Keep the authoritative transaction, locks, constraints, and direct SQL paths your application already owns.
|
|
28
28
|
</Card>
|
|
29
29
|
|
|
30
|
+
<Card title="Groups and shared context" icon="share-2" href="/groups">
|
|
31
|
+
Structure which state reaches each participant, from membership and access to loading, updates and removal.
|
|
32
|
+
</Card>
|
|
33
|
+
|
|
30
34
|
<Card title="Participant identity" icon="fingerprint" href="/identity">
|
|
31
35
|
Give agents, people, and services distinct scoped credentials instead of treating every worker as the same caller.
|
|
32
36
|
</Card>
|
|
@@ -172,7 +172,7 @@ export const ablo = Ablo({
|
|
|
172
172
|
|
|
173
173
|
Browser apps should use the React provider or a scoped session token, not a
|
|
174
174
|
server API key in the bundle. Build the client first, then hand it to the
|
|
175
|
-
provider — `AbloProvider` takes `{ client,
|
|
175
|
+
provider — `AbloProvider` takes `{ client, onError?, fallback? }`, and
|
|
176
176
|
nothing else (`schema`, `teamIds`, and `apiKey` all live on the
|
|
177
177
|
client now).
|
|
178
178
|
|
|
@@ -287,7 +287,7 @@ In React, selector `useAblo` is the public read API:
|
|
|
287
287
|
```tsx
|
|
288
288
|
'use client';
|
|
289
289
|
|
|
290
|
-
import { useAblo } from '@abloatai/ablo/react';
|
|
290
|
+
import { useAblo, useAbloClient } from '@abloatai/ablo/react';
|
|
291
291
|
|
|
292
292
|
export function ReportRow({
|
|
293
293
|
report: serverReport,
|
|
@@ -301,10 +301,10 @@ export function ReportRow({
|
|
|
301
301
|
}
|
|
302
302
|
```
|
|
303
303
|
|
|
304
|
-
|
|
304
|
+
Call `useAbloClient()` during render, then use its client in callbacks and effects:
|
|
305
305
|
|
|
306
306
|
```tsx
|
|
307
|
-
const ablo =
|
|
307
|
+
const ablo = useAbloClient();
|
|
308
308
|
```
|
|
309
309
|
|
|
310
310
|
## 4. Write State
|
package/docs/migration.md
CHANGED
|
@@ -3,7 +3,100 @@
|
|
|
3
3
|
> A safe workflow for upgrading the pre-1.0 SDK.
|
|
4
4
|
|
|
5
5
|
Ablo is pre-1.0, so a minor release may contain a breaking API change. Patch
|
|
6
|
-
releases remain compatible within the same minor line.
|
|
6
|
+
releases normally remain compatible within the same minor line. The 0.64.1
|
|
7
|
+
release includes the explicit React and collaboration migrations below.
|
|
8
|
+
|
|
9
|
+
## React and package boundaries after 0.64
|
|
10
|
+
|
|
11
|
+
Run `npx ablo upgrade` for migration hints before editing. It previews changes;
|
|
12
|
+
`--write` applies only mechanical rewrites. React changes requiring application
|
|
13
|
+
judgment are reported with their replacements.
|
|
14
|
+
|
|
15
|
+
| Previous API | Current API |
|
|
16
|
+
| --- | --- |
|
|
17
|
+
| `useSync()` or zero-argument `useAblo()` | `useAbloClient()` for actions; await `client.ready()` when initialization is required |
|
|
18
|
+
| `useSyncStatus()` | `useAblo(client => client.status)` |
|
|
19
|
+
| `usePeers()` | `useAblo(client => client.presence.others)` |
|
|
20
|
+
| Scoped peers | `usePresence(client => client.records, id, { excludeSelf: true })` |
|
|
21
|
+
| `useMutationFailureListener(listener)` | `useMutationFailure(listener)` |
|
|
22
|
+
| `useErrorListener()` | `AbloProvider`'s `onError` prop for startup errors |
|
|
23
|
+
| `useCurrentUserId()` | Your application's authentication context |
|
|
24
|
+
| `useSDKSyncStore()` / `useSyncStore()` | Public client operations; `getAbloStore(client)` from `/client` for custom local-store adapters |
|
|
25
|
+
| `ClientSideSuspense` / `DefaultFallback` | Application UI through the provider's `fallback` prop |
|
|
26
|
+
| `GroupScope` for presence | Model and record arguments |
|
|
27
|
+
|
|
28
|
+
`useAblo(selector)` returns detached snapshots for rendering. They have no model
|
|
29
|
+
methods or relation accessors. `useAbloClient()` returns the writable client and
|
|
30
|
+
does not subscribe to row changes. Do not perform writes inside a selector.
|
|
31
|
+
|
|
32
|
+
### Presence, activities and claims
|
|
33
|
+
|
|
34
|
+
`Activity` is replaced by `Ablo.PresenceActivity`, including for agents. A session
|
|
35
|
+
is `Ablo.PresenceSession`: identify the execution with `presenceSessionId` and its
|
|
36
|
+
actor with `participant.id` and `participant.kind`. A participant can have several
|
|
37
|
+
sessions, so exclude self by session rather than user ID.
|
|
38
|
+
|
|
39
|
+
`presence.active` contains this session's activities; `presence.others` contains
|
|
40
|
+
other sessions, each with an `activities` array. `forModel(model, id)` includes
|
|
41
|
+
self by default; pass `{ excludeSelf: true }` as its third argument to omit self.
|
|
42
|
+
The same option on `usePresence` still announces the component's reading activity
|
|
43
|
+
and cleans it up on unmount.
|
|
44
|
+
|
|
45
|
+
An activity has `operation`, `target`, `source`, and ISO timestamp fields.
|
|
46
|
+
Read the model and row from `activity.target.model` and `.id`; field-specific
|
|
47
|
+
activity uses `.field` or `.fields` (never both, and always with a row ID).
|
|
48
|
+
Activities describe visible work; they do not grant write authority.
|
|
49
|
+
|
|
50
|
+
Claims are acquired through `await using claim = await client.records.claim({ id })`.
|
|
51
|
+
The protected row is `claim.data`; pass `claim` to the write. Claim lookup uses
|
|
52
|
+
`client.records.claim.state({ id })` and `.queue({ id })`. Claim metadata is on
|
|
53
|
+
`claim.target.meta`, and a multi-field claim uses `fields` in the claim target.
|
|
54
|
+
Do not reconstruct a claim handle from a presence activity. See [Claims](./claims.md).
|
|
55
|
+
|
|
56
|
+
### Imports and schema ownership
|
|
57
|
+
|
|
58
|
+
The root factory creates a headless client; `/client` creates the reactive client.
|
|
59
|
+
Import the `Ablo` type namespace beside the factory you use. Both expose the
|
|
60
|
+
shared schema, presence-activity and claim types. Reactive-only types such as
|
|
61
|
+
`Ablo.Status`, `Ablo.Reads`, `Ablo.Store`, and `Ablo.Mutator` belong to `/client`
|
|
62
|
+
(or `/react` for its re-exported reactive factory).
|
|
63
|
+
|
|
64
|
+
Prefer explicit schema inference:
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
import { defineMutators } from '@abloatai/ablo/client';
|
|
68
|
+
import { useMutators, useUndoScope } from '@abloatai/ablo/react';
|
|
69
|
+
import { schema } from './schema';
|
|
70
|
+
|
|
71
|
+
const definitions = defineMutators(schema, {
|
|
72
|
+
records: {
|
|
73
|
+
rename: async ({ tx, args }: {
|
|
74
|
+
tx: import('@abloatai/ablo/client').Ablo.Mutator.Transaction<typeof schema>;
|
|
75
|
+
args: { id: string; title: string };
|
|
76
|
+
}) => tx.mutations.records.update({ id: args.id, title: args.title }),
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
function useRecordActions() {
|
|
81
|
+
const { scope } = useUndoScope(schema, 'record-editor');
|
|
82
|
+
return useMutators(schema, definitions, { undoScope: scope });
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Ambient registration remains available for a single application. Its module must
|
|
87
|
+
be included in that TypeScript program and import `@abloatai/ablo` before
|
|
88
|
+
augmenting `Register`. The public registration now reaches the downstream
|
|
89
|
+
Transaction and Humans resolvers through the emitted declarations. A package
|
|
90
|
+
compiled independently cannot inherit a consuming app's ambient declaration.
|
|
91
|
+
|
|
92
|
+
### Structural collaboration adapters
|
|
93
|
+
|
|
94
|
+
Generic `subscribe(event, handler)` preserves exactly the argument tuples in your
|
|
95
|
+
event map. Receive optional server attribution through the session's
|
|
96
|
+
`collaboration.subscribe(event, (payload, context) => ...)`; low-level transport
|
|
97
|
+
adapters use `subscribeCollaboration`. Model-scoped `client.records.events.on`
|
|
98
|
+
continues to supply authenticated context as its second callback argument.
|
|
99
|
+
Legacy servers may omit context on application events; handle `undefined` there.
|
|
7
100
|
|
|
8
101
|
## Upgrade safely
|
|
9
102
|
|
|
@@ -14,7 +107,7 @@ releases remain compatible within the same minor line.
|
|
|
14
107
|
5. Run the three-state deployment plan, then push the exact reviewed plan.
|
|
15
108
|
|
|
16
109
|
```bash
|
|
17
|
-
npm install @abloatai/ablo@
|
|
110
|
+
npm install --save-exact @abloatai/ablo@<version> @abloatai/cli@<version>
|
|
18
111
|
npx ablo docs
|
|
19
112
|
npx ablo docs api
|
|
20
113
|
npx ablo plan
|
package/docs/quickstart.md
CHANGED
|
@@ -16,6 +16,12 @@ schema** — your migration tool stays in charge of the shape of your database.
|
|
|
16
16
|
> or separate database, and connect your production one when you're ready for it
|
|
17
17
|
> to be the system of record.
|
|
18
18
|
|
|
19
|
+
Groups structure which shared state reaches each person or agent. Declare the
|
|
20
|
+
data's scope and participant authority, then let authorized subscriptions keep
|
|
21
|
+
reactive views current; HTTP agents explicitly read or observe changes. Start
|
|
22
|
+
with [Groups and shared context](./groups.md) to connect membership, access,
|
|
23
|
+
loading and updates.
|
|
24
|
+
|
|
19
25
|
## 1. Install and initialize
|
|
20
26
|
|
|
21
27
|
```bash
|
|
@@ -82,6 +88,7 @@ every type is one parameter away — no `typeof schema` re-stating, anywhere:
|
|
|
82
88
|
|
|
83
89
|
```ts
|
|
84
90
|
// ablo/register.ts — scaffolded by `npx ablo init`, sits beside ablo/schema.ts
|
|
91
|
+
import type {} from '@abloatai/ablo';
|
|
85
92
|
import type { schema } from './schema';
|
|
86
93
|
declare module '@abloatai/ablo' {
|
|
87
94
|
interface Register { Schema: typeof schema }
|
|
@@ -89,11 +96,11 @@ declare module '@abloatai/ablo' {
|
|
|
89
96
|
export {};
|
|
90
97
|
```
|
|
91
98
|
|
|
92
|
-
It's a regular `.ts` module, not a hand-authored `.d.ts`. The
|
|
93
|
-
`import type {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
`tsconfig` `include` works; it never needs
|
|
99
|
+
It's a regular `.ts` module, not a hand-authored `.d.ts`. The empty
|
|
100
|
+
`import type {} from '@abloatai/ablo'` loads the SDK's module declaration so the
|
|
101
|
+
`declare module` block augments its existing `Register` interface. The schema
|
|
102
|
+
import supplies your schema's type. Both imports are erased at runtime. Any `.ts`
|
|
103
|
+
file in your `tsconfig` `include` works; it never needs a runtime import.
|
|
97
104
|
|
|
98
105
|
```ts
|
|
99
106
|
import type { Model } from '@abloatai/ablo/schema';
|
package/docs/react.md
CHANGED
|
@@ -13,7 +13,7 @@ agents, start with [Integration Guide](/docs/integration-guide).
|
|
|
13
13
|
The React bindings ship with the main package — no extra install.
|
|
14
14
|
|
|
15
15
|
```ts
|
|
16
|
-
import { useAblo } from '@abloatai/ablo/react';
|
|
16
|
+
import { useAblo, useAbloClient } from '@abloatai/ablo/react';
|
|
17
17
|
```
|
|
18
18
|
|
|
19
19
|
React uses the same objects and operations as core Ablo. Read `ablo.status`
|
|
@@ -21,11 +21,12 @@ with `useAblo(ablo => ablo.status)`, select `ablo.presence.others` for other
|
|
|
21
21
|
sessions, and call ordinary model methods from event handlers. React owns the
|
|
22
22
|
subscription or component lifetime; the client owns the behavior.
|
|
23
23
|
|
|
24
|
-
For mutation failures,
|
|
24
|
+
For mutation failures, let the hook own the subscription:
|
|
25
25
|
|
|
26
26
|
```tsx
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
import { useMutationFailure } from '@abloatai/ablo/react';
|
|
28
|
+
|
|
29
|
+
useMutationFailure(reportFailure);
|
|
29
30
|
```
|
|
30
31
|
|
|
31
32
|
`reportFailure` receives `Ablo.MutationFailure`. Read application identity from
|
|
@@ -85,12 +86,12 @@ export const ablo = Ablo({
|
|
|
85
86
|
});
|
|
86
87
|
|
|
87
88
|
// The typed binding: capture the schema once, and every component imports
|
|
88
|
-
// born-typed hooks from this file — `
|
|
89
|
+
// born-typed hooks from this file — `useAbloClient()` takes no type arguments,
|
|
89
90
|
// and a selector's `ablo` parameter knows your models.
|
|
90
|
-
export const { AbloProvider, useAblo, usePresence } = createAbloReact(schema);
|
|
91
|
+
export const { AbloProvider, useAblo, useAbloClient, usePresence, useMutationFailure } = createAbloReact(schema);
|
|
91
92
|
```
|
|
92
93
|
|
|
93
|
-
Import `AbloProvider`, `useAblo`, and `usePresence` from `lib/ablo` rather than from the
|
|
94
|
+
Import `AbloProvider`, `useAbloClient`, `useAblo`, and `usePresence` from `lib/ablo` rather than from the
|
|
94
95
|
package, and the schema generic never appears at a call site again — the
|
|
95
96
|
same one-binding-file convention as tRPC's `createTRPCReact` or
|
|
96
97
|
react-redux's typed hooks.
|
|
@@ -109,25 +110,24 @@ import { ablo } from '@/lib/ablo';
|
|
|
109
110
|
|
|
110
111
|
export function Providers({
|
|
111
112
|
children,
|
|
112
|
-
user, // resolved server-side from YOUR auth
|
|
113
113
|
}: {
|
|
114
114
|
children: React.ReactNode;
|
|
115
|
-
user: { id: string };
|
|
116
115
|
}) {
|
|
117
116
|
return (
|
|
118
|
-
<AbloProvider client={ablo}
|
|
117
|
+
<AbloProvider client={ablo} fallback={<AppSkeleton />}>
|
|
119
118
|
{children}
|
|
120
119
|
</AbloProvider>
|
|
121
120
|
);
|
|
122
121
|
}
|
|
123
122
|
```
|
|
124
123
|
|
|
125
|
-
`client` is the only required prop. The
|
|
124
|
+
`client` is the only required prop. The removed `userId` prop is no longer accepted;
|
|
125
|
+
read application identity from your authentication context. Ablo authority comes
|
|
126
|
+
from the client session. The remaining props are situational:
|
|
126
127
|
|
|
127
128
|
| Prop | Default | Purpose |
|
|
128
129
|
| ----------- | ---------------- | --------------------------------------------------------------------------------------------------------- |
|
|
129
130
|
| `client` |: | **Required.** The `Ablo({ schema, apiKey })` instance. It carries the schema and connection config. |
|
|
130
|
-
| `userId` | resolved from auth | App participant id for app-owned fields and your `identityRoles`. Not the security boundary. |
|
|
131
131
|
| `fallback` | neutral spinner | Rendered during the *first* bootstrap only. Pass a branded skeleton, `null`, or `'passthrough'`. |
|
|
132
132
|
| `onError` |: | Engine / WebSocket / bootstrap errors. Wire to Sentry / Datadog. |
|
|
133
133
|
|
|
@@ -145,7 +145,7 @@ session owner calls `await ablo.dispose()` on logout or before replacing that
|
|
|
145
145
|
client. Provider remounts can reuse it. Never share a browser singleton across
|
|
146
146
|
server requests. When changing accounts, remove the old account UI and create a
|
|
147
147
|
fresh client whose session endpoint grants the newly verified membership.
|
|
148
|
-
|
|
148
|
+
Query filters and application identity state do not change authorization.
|
|
149
149
|
|
|
150
150
|
For a component-owned client, create and dispose the instance in the same effect.
|
|
151
151
|
React Strict Mode can replay setup and cleanup, so each setup creates a fresh
|
|
@@ -252,7 +252,7 @@ Use the zero-argument form only when you need the full client for callbacks,
|
|
|
252
252
|
effects, or writes:
|
|
253
253
|
|
|
254
254
|
```tsx
|
|
255
|
-
const abloClient =
|
|
255
|
+
const abloClient = useAbloClient();
|
|
256
256
|
```
|
|
257
257
|
|
|
258
258
|
Prefer selector reads like `useAblo((ablo) => ablo.<model>.local.get(id))`. Older hooks
|
|
@@ -302,7 +302,7 @@ For client event handlers, get the application-owned client and call the same
|
|
|
302
302
|
model client:
|
|
303
303
|
|
|
304
304
|
```tsx
|
|
305
|
-
const ablo =
|
|
305
|
+
const ablo = useAbloClient();
|
|
306
306
|
|
|
307
307
|
async function markReady() {
|
|
308
308
|
if (!ablo) return;
|
|
@@ -376,7 +376,7 @@ signals. The model and record choose the authorized sync group; the payload
|
|
|
376
376
|
does not need routing fields or caller-authored identity.
|
|
377
377
|
|
|
378
378
|
```tsx
|
|
379
|
-
const ablo =
|
|
379
|
+
const ablo = useAbloClient();
|
|
380
380
|
|
|
381
381
|
useEffect(() => {
|
|
382
382
|
if (!ablo) return;
|
|
@@ -431,3 +431,46 @@ its returned cleanup function.
|
|
|
431
431
|
|
|
432
432
|
The Next.js [App Router landing](./examples/nextjs.md) walks through Server Components
|
|
433
433
|
+ Server Actions + `useAblo` together.
|
|
434
|
+
|
|
435
|
+
## Separate packages and monorepos
|
|
436
|
+
|
|
437
|
+
Put the schema in an application-owned package, with a public export for its
|
|
438
|
+
value and type. Put the React binding in a client module which imports that
|
|
439
|
+
schema, and export its inferred declarations. Components import their hooks from
|
|
440
|
+
that binding package. Build the schema package before its dependents and test the
|
|
441
|
+
emitted declarations without workspace source aliases.
|
|
442
|
+
|
|
443
|
+
```ts
|
|
444
|
+
'use client';
|
|
445
|
+
import { createAbloReact } from '@abloatai/ablo/react';
|
|
446
|
+
import { schema } from '@app/schema';
|
|
447
|
+
|
|
448
|
+
export const { AbloProvider, useAbloClient, useAblo, usePresence, useMutationFailure } =
|
|
449
|
+
createAbloReact(schema);
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
Use one binding per schema, and mount its hooks under a provider for that schema.
|
|
453
|
+
The binding specializes types; it does not create an isolated runtime context or
|
|
454
|
+
validate that a different binding's provider has the same schema. Keep React and
|
|
455
|
+
the Ablo package family deduplicated across packages. Reusable libraries should
|
|
456
|
+
accept a schema or typed binding rather than declare an application-global schema.
|
|
457
|
+
An app's ambient registration cannot retroactively change a separately compiled
|
|
458
|
+
library. Use `defineMutators(schema, definitions)`, `useMutators(schema, definitions)`
|
|
459
|
+
and `useUndoScope(schema, name)` across those boundaries.
|
|
460
|
+
|
|
461
|
+
## Mutation failures and scoped collaborators
|
|
462
|
+
|
|
463
|
+
```tsx
|
|
464
|
+
const peers = usePresence(client => client.records, id, { excludeSelf: true });
|
|
465
|
+
useMutationFailure(({ error }) => showToast(error.message));
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
`useMutationFailure` uses the latest committed callback, moves its subscription
|
|
469
|
+
when the provider client changes, and unsubscribes on unmount. For non-React
|
|
470
|
+
callers, `client.onMutationFailure` returns the cleanup function.
|
|
471
|
+
|
|
472
|
+
For a custom framework adapter, `getAbloStore(client)` from
|
|
473
|
+
`@abloatai/ablo/client` returns the supported `Ablo.Store` contract: local pool
|
|
474
|
+
access, mutation observation, and scope management. Ordinary writes, confirmation,
|
|
475
|
+
reconnection and model events already have public client methods; they do not
|
|
476
|
+
require a store adapter.
|
package/llms.txt
CHANGED
|
@@ -174,7 +174,7 @@ coordination until the app reports it through Data Source events.
|
|
|
174
174
|
|
|
175
175
|
## Change propagation
|
|
176
176
|
|
|
177
|
-
A change to one row reaches other rows three ways. ROUTING: a write fans out to every sync group the row belongs to,
|
|
177
|
+
A change to one row reaches other rows three ways. ROUTING: a write fans out to every sync group the row belongs to, including declared ancestor routes for models without `subject`; subject-scoped rows route exclusively through their required subject group, so only eligible subscribers receive them — delivery, not recomputation. DELETE CASCADE: deleting a parent emits explicit tombstone deltas for its descendants, so open clients never silently hold rows that are gone. VALUE: derived values are NOT recomputed server-side — Ablo surfaces that the source moved and the actor decides. To keep dependent work fresh, pass rows returned by `ablo.<model>.read({ id })` in the mutation's `reads` array. Ablo records compact model/id/readAt evidence, not row contents. At commit the server checks whether anything moved past the read watermark; if so, the mutation does not land. Use `get` when no such relationship exists. To chain A→B→C, put A+B in one group and B+C in another: A's change reaches B, and C hears it only once B ITSELF writes. No transitive auto-recompute, no convergence guarantee for cycles.
|
|
178
178
|
|
|
179
179
|
## Nouns
|
|
180
180
|
|
|
@@ -322,3 +322,7 @@ Canonical docs to read before integrating, in this order. Read each with `npx ab
|
|
|
322
322
|
- [Upgrade Guide](https://docs.abloatai.com/migration): when upgrading an existing integration; every breaking change, what to change, and which version introduced it.
|
|
323
323
|
- [Session Settings](https://docs.abloatai.com/session-settings): when the customer's database has row-level-security policies; the identity context Ablo sets before every write, and how to map it to the setting names those policies read.
|
|
324
324
|
- [Every page, one line each](https://docs.abloatai.com/llms.txt), or [the full docs as one file](https://docs.abloatai.com/llms-full.txt).
|
|
325
|
+
|
|
326
|
+
## Groups and shared context
|
|
327
|
+
|
|
328
|
+
Groups connect data membership to authorized participant subscriptions. Model roots, relations and grants describe routing and membership; policy, subject and capability operations establish access. Subject-scoped rows route exclusively through their subject group. Reactive clients load and maintain local state; HTTP agents read or observe changes explicitly. Group removal currently clears the managed client cache and requests re-bootstrap, rather than selectively evicting rows. Membership does not imply loaded data, activity, or a lock. Start with `ablo docs groups` for the lifecycle and the account multiplayer example; use `ablo docs identity` for authentication and credential issuance.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@abloatai/ablo",
|
|
3
|
-
"version": "0.64.
|
|
3
|
+
"version": "0.64.2",
|
|
4
4
|
"description": "The public Ablo SDK for coordinated reads, commits, claims, observation, and reactive applications.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -132,7 +132,8 @@
|
|
|
132
132
|
"build:docs": "node scripts/build-blume-docs.mjs",
|
|
133
133
|
"lint:docs-site": "node scripts/build-blume-docs.mjs --check",
|
|
134
134
|
"lint:docs": "node scripts/check-doc-drift.mjs",
|
|
135
|
-
"lint:pkg": "publint"
|
|
135
|
+
"lint:pkg": "publint",
|
|
136
|
+
"verify:package-contract": "node scripts/package-contract/index.mjs"
|
|
136
137
|
},
|
|
137
138
|
"publishConfig": {
|
|
138
139
|
"access": "public",
|
|
@@ -145,8 +146,8 @@
|
|
|
145
146
|
"directory": "packages/ablo"
|
|
146
147
|
},
|
|
147
148
|
"dependencies": {
|
|
148
|
-
"@abloatai/humans": "
|
|
149
|
-
"@abloatai/transaction": "
|
|
149
|
+
"@abloatai/humans": "0.64.2",
|
|
150
|
+
"@abloatai/transaction": "0.64.2",
|
|
150
151
|
"zod": "^4.4.3"
|
|
151
152
|
},
|
|
152
153
|
"peerDependencies": {
|