@thomasfosterau/pulpit 0.0.0 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +1271 -1
- package/dist/blobs.d.ts +249 -0
- package/dist/blobs.d.ts.map +1 -0
- package/dist/blobs.js +193 -0
- package/dist/blobs.js.map +1 -0
- package/dist/budgets.d.ts +255 -0
- package/dist/budgets.d.ts.map +1 -0
- package/dist/budgets.js +211 -0
- package/dist/budgets.js.map +1 -0
- package/dist/cache.d.ts +154 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +118 -0
- package/dist/cache.js.map +1 -0
- package/dist/collections.d.ts +242 -0
- package/dist/collections.d.ts.map +1 -0
- package/dist/collections.js +215 -0
- package/dist/collections.js.map +1 -0
- package/dist/comments.d.ts +212 -0
- package/dist/comments.d.ts.map +1 -0
- package/dist/comments.js +192 -0
- package/dist/comments.js.map +1 -0
- package/dist/content.d.ts +231 -0
- package/dist/content.d.ts.map +1 -0
- package/dist/content.js +204 -0
- package/dist/content.js.map +1 -0
- package/dist/documents.d.ts +184 -0
- package/dist/documents.d.ts.map +1 -0
- package/dist/documents.js +190 -0
- package/dist/documents.js.map +1 -0
- package/dist/edges.d.ts +645 -0
- package/dist/edges.d.ts.map +1 -0
- package/dist/edges.js +558 -0
- package/dist/edges.js.map +1 -0
- package/dist/filter.d.ts +461 -0
- package/dist/filter.d.ts.map +1 -0
- package/dist/filter.js +387 -0
- package/dist/filter.js.map +1 -0
- package/dist/index.d.ts +133 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +133 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/filter-columns.d.ts +8 -0
- package/dist/internal/filter-columns.d.ts.map +1 -0
- package/dist/internal/filter-columns.js +43 -0
- package/dist/internal/filter-columns.js.map +1 -0
- package/dist/internal/list-order.d.ts +26 -0
- package/dist/internal/list-order.d.ts.map +1 -0
- package/dist/internal/list-order.js +92 -0
- package/dist/internal/list-order.js.map +1 -0
- package/dist/internal/owned-columns.d.ts +93 -0
- package/dist/internal/owned-columns.d.ts.map +1 -0
- package/dist/internal/owned-columns.js +98 -0
- package/dist/internal/owned-columns.js.map +1 -0
- package/dist/internal/plain-rows.d.ts +32 -0
- package/dist/internal/plain-rows.d.ts.map +1 -0
- package/dist/internal/plain-rows.js +32 -0
- package/dist/internal/plain-rows.js.map +1 -0
- package/dist/internal/reference-columns.d.ts +80 -0
- package/dist/internal/reference-columns.d.ts.map +1 -0
- package/dist/internal/reference-columns.js +102 -0
- package/dist/internal/reference-columns.js.map +1 -0
- package/dist/moderation.d.ts +392 -0
- package/dist/moderation.d.ts.map +1 -0
- package/dist/moderation.js +375 -0
- package/dist/moderation.js.map +1 -0
- package/dist/paths.d.ts +204 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +165 -0
- package/dist/paths.js.map +1 -0
- package/dist/proposals.d.ts +523 -0
- package/dist/proposals.d.ts.map +1 -0
- package/dist/proposals.js +505 -0
- package/dist/proposals.js.map +1 -0
- package/dist/publication.d.ts +148 -0
- package/dist/publication.d.ts.map +1 -0
- package/dist/publication.js +183 -0
- package/dist/publication.js.map +1 -0
- package/dist/purge.d.ts +224 -0
- package/dist/purge.d.ts.map +1 -0
- package/dist/purge.js +151 -0
- package/dist/purge.js.map +1 -0
- package/dist/reactions.d.ts +246 -0
- package/dist/reactions.d.ts.map +1 -0
- package/dist/reactions.js +183 -0
- package/dist/reactions.js.map +1 -0
- package/dist/registry.d.ts +316 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +305 -0
- package/dist/registry.js.map +1 -0
- package/dist/search.d.ts +366 -0
- package/dist/search.d.ts.map +1 -0
- package/dist/search.js +273 -0
- package/dist/search.js.map +1 -0
- package/dist/social.d.ts +311 -0
- package/dist/social.d.ts.map +1 -0
- package/dist/social.js +205 -0
- package/dist/social.js.map +1 -0
- package/dist/subtype.d.ts +1613 -0
- package/dist/subtype.d.ts.map +1 -0
- package/dist/subtype.js +1300 -0
- package/dist/subtype.js.map +1 -0
- package/dist/testing.d.ts +675 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +1022 -0
- package/dist/testing.js.map +1 -0
- package/dist/version-chain.d.ts +702 -0
- package/dist/version-chain.d.ts.map +1 -0
- package/dist/version-chain.js +489 -0
- package/dist/version-chain.js.map +1 -0
- package/dist/working-copy.d.ts +116 -0
- package/dist/working-copy.d.ts.map +1 -0
- package/dist/working-copy.js +95 -0
- package/dist/working-copy.js.map +1 -0
- package/package.json +138 -4
- package/src/blobs.ts +384 -0
- package/src/budgets.ts +338 -0
- package/src/cache.ts +218 -0
- package/src/collections.ts +427 -0
- package/src/comments.ts +392 -0
- package/src/content.ts +238 -0
- package/src/documents.ts +284 -0
- package/src/edges.ts +1185 -0
- package/src/filter.ts +523 -0
- package/src/index.ts +153 -0
- package/src/internal/filter-columns.ts +50 -0
- package/src/internal/list-order.ts +117 -0
- package/src/internal/owned-columns.ts +140 -0
- package/src/internal/plain-rows.ts +54 -0
- package/src/internal/reference-columns.ts +156 -0
- package/src/moderation.ts +728 -0
- package/src/paths.ts +267 -0
- package/src/proposals.ts +1014 -0
- package/src/publication.ts +225 -0
- package/src/purge.ts +313 -0
- package/src/reactions.ts +347 -0
- package/src/registry.ts +524 -0
- package/src/search.ts +540 -0
- package/src/social.ts +469 -0
- package/src/subtype.ts +3095 -0
- package/src/testing.ts +1873 -0
- package/src/version-chain.ts +1093 -0
- package/src/working-copy.ts +192 -0
package/dist/cache.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* **The reference resource cache** (pulpit#205) — `./subtype`'s
|
|
3
|
+
* {@link ResourceCacheSeam}, with a read-through `get`, over a minimal
|
|
4
|
+
* versioned key-value seam ({@link VersionedKv}).
|
|
5
|
+
*
|
|
6
|
+
* The behavioural spec is the four apps' own caches (website's, gardeneria's,
|
|
7
|
+
* preferenceflow's and in-the-dark's `apps/api/src/cache.ts`), which this
|
|
8
|
+
* replaces: one implementation of the contract the kernel already states.
|
|
9
|
+
*
|
|
10
|
+
* - **Compare-on-set** (pulpit#121). Every entry carries the version it
|
|
11
|
+
* speaks for, and the KV seam's one conditional write keeps the highest:
|
|
12
|
+
* a `set` or `delete` below the stored version changes nothing, and an
|
|
13
|
+
* eviction raises the version, so a racing writer's late write-through can
|
|
14
|
+
* never re-cache a resource older than one already replaced or evicted.
|
|
15
|
+
* - **An eviction wins a tie.** A tombstone outranks a value at the same
|
|
16
|
+
* version: only a strictly newer write clears it, so a read-through that
|
|
17
|
+
* loaded the row before an unpublish at that version can't re-cache it.
|
|
18
|
+
* The cache stores a rank, `2·version` for a value and `2·version + 1` for
|
|
19
|
+
* a tombstone, as the entry's `version`, so the seam still compares one
|
|
20
|
+
* number.
|
|
21
|
+
* - **Tombstones outlive values.** A tombstone is kept for
|
|
22
|
+
* `tombstoneTtlSeconds` (by default a day, and never less than the value
|
|
23
|
+
* TTL), which must exceed the longest a write can be in flight: a late
|
|
24
|
+
* write-through that arrives after its tombstone has expired is no longer
|
|
25
|
+
* ordered against it.
|
|
26
|
+
* - **Serveability on both sides** (`CACHE-AUTH-005`). `set` refuses (and
|
|
27
|
+
* evicts) a resource the `serveable` policy refuses, and `get` checks it
|
|
28
|
+
* again, so a policy change can't serve a stale cached private row.
|
|
29
|
+
* - **Decode or evict.** `get` decodes the stored value with the resource's
|
|
30
|
+
* schema; a value that no longer decodes (the schema moved) is evicted and
|
|
31
|
+
* read as a miss.
|
|
32
|
+
* - **A TTL backstop.** Every write carries the configured TTL, so an entry
|
|
33
|
+
* no write ever evicted still expires.
|
|
34
|
+
* - **Never fails.** A KV failure reads as a miss and skips a write: the
|
|
35
|
+
* cache is an optimisation, and the store stays the source of truth.
|
|
36
|
+
*
|
|
37
|
+
* The KV seam is the application's (a Durable Object, Redis, a SQL table):
|
|
38
|
+
* nothing here imports a platform. Workers KV alone has no conditional
|
|
39
|
+
* write, so it can't fill {@link VersionedKv.putIfNotBehind}.
|
|
40
|
+
*
|
|
41
|
+
* @copyright Thomas Foster
|
|
42
|
+
* @module pulpit/cache
|
|
43
|
+
* @since 0.1.0
|
|
44
|
+
*/
|
|
45
|
+
import * as Effect from "effect/Effect";
|
|
46
|
+
import * as Option from "effect/Option";
|
|
47
|
+
import * as Schema from "effect/Schema";
|
|
48
|
+
/**
|
|
49
|
+
* The default time an eviction's tombstone is kept: one day, far longer than
|
|
50
|
+
* any request can be in flight.
|
|
51
|
+
*
|
|
52
|
+
* @since 0.1.0
|
|
53
|
+
*/
|
|
54
|
+
export const DEFAULT_TOMBSTONE_TTL_SECONDS = 86_400;
|
|
55
|
+
/**
|
|
56
|
+
* The reference {@link ResourceCacheSeam} over a {@link VersionedKv}.
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* ```ts
|
|
60
|
+
* import { deepStrictEqual } from "node:assert";
|
|
61
|
+
* import * as Effect from "effect/Effect";
|
|
62
|
+
* import * as Option from "effect/Option";
|
|
63
|
+
* import * as Schema from "effect/Schema";
|
|
64
|
+
* import { makeResourceCache } from "@thomasfosterau/pulpit/cache";
|
|
65
|
+
* import { makeInMemoryVersionedKv } from "@thomasfosterau/pulpit/testing";
|
|
66
|
+
*
|
|
67
|
+
* const Post = Schema.Struct({ id: Schema.String, visibility: Schema.String });
|
|
68
|
+
* const cache = makeResourceCache({
|
|
69
|
+
* kv: makeInMemoryVersionedKv(),
|
|
70
|
+
* schema: Post,
|
|
71
|
+
* serveable: (post) => post.visibility === "public",
|
|
72
|
+
* prefix: "posts:v1:",
|
|
73
|
+
* ttlSeconds: 3600,
|
|
74
|
+
* });
|
|
75
|
+
* const program = Effect.gen(function* () {
|
|
76
|
+
* yield* cache.set("p-1", { id: "p-1", visibility: "public" }, 1n);
|
|
77
|
+
* yield* cache.set("p-1", { id: "p-1", visibility: "private" }, 0n); // behind: no-op
|
|
78
|
+
* return yield* cache.get("p-1");
|
|
79
|
+
* });
|
|
80
|
+
* deepStrictEqual(Effect.runSync(program), Option.some({ id: "p-1", visibility: "public" }));
|
|
81
|
+
* ```
|
|
82
|
+
*
|
|
83
|
+
* @since 0.1.0
|
|
84
|
+
*/
|
|
85
|
+
export const makeResourceCache = (options) => {
|
|
86
|
+
const { kv, serveable, prefix } = options;
|
|
87
|
+
const ttl = Math.max(1, Math.trunc(options.ttlSeconds) || 1);
|
|
88
|
+
const tombstoneTtl = Math.max(ttl, Math.trunc(options.tombstoneTtlSeconds ?? DEFAULT_TOMBSTONE_TTL_SECONDS) || ttl);
|
|
89
|
+
const encode = Schema.encodeOption(options.schema);
|
|
90
|
+
const decode = Schema.decodeUnknownOption(options.schema);
|
|
91
|
+
const keyOf = (id) => `${prefix}${id}`;
|
|
92
|
+
/**
|
|
93
|
+
* The conditional write at the entry's rank (`2·version`, plus one for a
|
|
94
|
+
* tombstone, so an eviction wins a tie), a KV failure skipped (the cache
|
|
95
|
+
* never fails). A tombstone carries the longer TTL.
|
|
96
|
+
*/
|
|
97
|
+
const write = (id, version, value) => Effect.ignore(kv.putIfNotBehind(keyOf(id), { version: version * 2n + (Option.isNone(value) ? 1n : 0n), value }, Option.isNone(value) ? tombstoneTtl : ttl));
|
|
98
|
+
const evict = (id, version) => write(id, version, Option.none());
|
|
99
|
+
const set = (id, resource, version) => {
|
|
100
|
+
// Refuse (and evict) what the policy doesn't serve, or what doesn't encode.
|
|
101
|
+
const encoded = serveable(resource) ? encode(resource) : Option.none();
|
|
102
|
+
return write(id, version, encoded);
|
|
103
|
+
};
|
|
104
|
+
const get = (id) => Effect.gen(function* () {
|
|
105
|
+
const stored = yield* Effect.orElseSucceed(kv.get(keyOf(id)), () => Option.none());
|
|
106
|
+
if (Option.isNone(stored) || Option.isNone(stored.value.value))
|
|
107
|
+
return Option.none();
|
|
108
|
+
const resource = decode(stored.value.value.value);
|
|
109
|
+
if (Option.isNone(resource) || !serveable(resource.value)) {
|
|
110
|
+
// Decode or evict, and never serve what the policy now refuses (CACHE-AUTH-005).
|
|
111
|
+
yield* evict(id, stored.value.version / 2n);
|
|
112
|
+
return Option.none();
|
|
113
|
+
}
|
|
114
|
+
return resource;
|
|
115
|
+
});
|
|
116
|
+
return { set, delete: evict, get };
|
|
117
|
+
};
|
|
118
|
+
//# sourceMappingURL=cache.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache.js","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* **Ordered collections** (pulpit#193) — positioned members of a parent
|
|
3
|
+
* node: a list's items, an album's media, a byline's authors, a menu's
|
|
4
|
+
* entries.
|
|
5
|
+
*
|
|
6
|
+
* The behavioural spec is website's ordered list items (a reorder appends
|
|
7
|
+
* a parent version) and in-the-dark's list entries (an updatable position
|
|
8
|
+
* beside the chain), with their tests.
|
|
9
|
+
*
|
|
10
|
+
* ## The model (owner decision on pulpit#193)
|
|
11
|
+
*
|
|
12
|
+
* Both models, opt-in per family, over **one store seam**
|
|
13
|
+
* ({@link CollectionStore}), which holds each member's position:
|
|
14
|
+
*
|
|
15
|
+
* - **`mutable`** (the default): the order is state beside the chain, like
|
|
16
|
+
* an edge. A write changes the store and nothing else.
|
|
17
|
+
* - **`versioned`**: every write also appends a parent version through the
|
|
18
|
+
* parent leaf's own `update`, recording the whole order (the member ids,
|
|
19
|
+
* in order) in a declared parent column, so `VERSION-CHAIN-001` covers it
|
|
20
|
+
* and history shows each reorder. After the parent's `restoreVersion`,
|
|
21
|
+
* {@link CollectionOps.resync} replaces the store's order with the one
|
|
22
|
+
* the restored version records, keeping only the members the writer can
|
|
23
|
+
* read, as `append` does.
|
|
24
|
+
*
|
|
25
|
+
* That column holds the id of every member, the ones a reader of the
|
|
26
|
+
* parent may not be able to read included, so it must stay out of the
|
|
27
|
+
* parent's viewer-facing resource (`EDGE-001`): wrap the parent leaf's
|
|
28
|
+
* `toResource` with {@link hideRecordedOrder}. The parent's history
|
|
29
|
+
* (`getVersion`, `compare`) still shows it, behind the write gate
|
|
30
|
+
* (`READ-009`).
|
|
31
|
+
*
|
|
32
|
+
* ## The gates
|
|
33
|
+
*
|
|
34
|
+
* - **Reads** go through the parent's read gate (`get`): a hidden parent is
|
|
35
|
+
* `NodeNotFound`, never `Forbidden` (`SEC-AUTH-ENUM-001`). Each member is
|
|
36
|
+
* read through the member leaf's own gate (`getMany`), and one the viewer
|
|
37
|
+
* can't read is omitted, from the page and from its total, as `EDGE-001`
|
|
38
|
+
* omits an edge with a hidden end.
|
|
39
|
+
* - **Writes** pass the parent's write gate (`authorize`), and a member must
|
|
40
|
+
* be one the writer can read (`NodeNotFound` otherwise).
|
|
41
|
+
* - A collection holds at most {@link MAX_COLLECTION_SIZE} members, so every
|
|
42
|
+
* read and reorder is one bounded read (`SEC-INPUT-003`).
|
|
43
|
+
*
|
|
44
|
+
* @copyright Thomas Foster
|
|
45
|
+
* @module pulpit/collections
|
|
46
|
+
* @since 0.1.0
|
|
47
|
+
*/
|
|
48
|
+
import * as Effect from "effect/Effect";
|
|
49
|
+
import * as Schema from "effect/Schema";
|
|
50
|
+
import { type Forbidden, type LeafRow, type Page, type SubtypeOps, type Unauthorized, type WriteRejected } from "./subtype.ts";
|
|
51
|
+
import { NodeNotFound, type StaleVersion, type VersionChainStore, type VersionConflict } from "./version-chain.ts";
|
|
52
|
+
/**
|
|
53
|
+
* The most members a collection holds (`SEC-INPUT-003`): every read and
|
|
54
|
+
* reorder reads the whole order once, so it is bounded.
|
|
55
|
+
*
|
|
56
|
+
* @since 0.1.0
|
|
57
|
+
*/
|
|
58
|
+
export declare const MAX_COLLECTION_SIZE = 1000;
|
|
59
|
+
declare const AlreadyMember_base: Schema.Class<AlreadyMember, Schema.TaggedStruct<"AlreadyMember", {
|
|
60
|
+
readonly parentId: Schema.String;
|
|
61
|
+
readonly memberId: Schema.String;
|
|
62
|
+
}>, import("effect/Cause").YieldableError & {
|
|
63
|
+
readonly brand: unique symbol;
|
|
64
|
+
}>;
|
|
65
|
+
/**
|
|
66
|
+
* The member is already in the collection: the wire's 409.
|
|
67
|
+
*
|
|
68
|
+
* @since 0.1.0
|
|
69
|
+
*/
|
|
70
|
+
export declare class AlreadyMember extends AlreadyMember_base {
|
|
71
|
+
}
|
|
72
|
+
declare const NotMember_base: Schema.Class<NotMember, Schema.TaggedStruct<"NotMember", {
|
|
73
|
+
readonly parentId: Schema.String;
|
|
74
|
+
readonly memberId: Schema.String;
|
|
75
|
+
}>, import("effect/Cause").YieldableError & {
|
|
76
|
+
readonly brand: unique symbol;
|
|
77
|
+
}>;
|
|
78
|
+
/**
|
|
79
|
+
* The member is not in the collection: the wire's 404.
|
|
80
|
+
*
|
|
81
|
+
* @since 0.1.0
|
|
82
|
+
*/
|
|
83
|
+
export declare class NotMember extends NotMember_base {
|
|
84
|
+
}
|
|
85
|
+
declare const CollectionFull_base: Schema.Class<CollectionFull, Schema.TaggedStruct<"CollectionFull", {
|
|
86
|
+
readonly parentId: Schema.String;
|
|
87
|
+
readonly max: Schema.Finite;
|
|
88
|
+
}>, import("effect/Cause").YieldableError & {
|
|
89
|
+
readonly brand: unique symbol;
|
|
90
|
+
}>;
|
|
91
|
+
/**
|
|
92
|
+
* The collection already holds {@link MAX_COLLECTION_SIZE} members: the
|
|
93
|
+
* wire's 409.
|
|
94
|
+
*
|
|
95
|
+
* @since 0.1.0
|
|
96
|
+
*/
|
|
97
|
+
export declare class CollectionFull extends CollectionFull_base {
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* One member's place in a collection.
|
|
101
|
+
*
|
|
102
|
+
* @since 0.1.0
|
|
103
|
+
*/
|
|
104
|
+
export interface CollectionEntry {
|
|
105
|
+
readonly memberId: string;
|
|
106
|
+
readonly position: number;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* The membership table an application supplies, keyed
|
|
110
|
+
* `(parent_id, member_id)` with both foreign keys cascading. A store seam:
|
|
111
|
+
* no policy, never opens a transaction, fails only with its `E`. See the
|
|
112
|
+
* package README → Seams.
|
|
113
|
+
*
|
|
114
|
+
* @since 0.1.0
|
|
115
|
+
*/
|
|
116
|
+
export interface CollectionStore<E, R> {
|
|
117
|
+
/**
|
|
118
|
+
* The parent's members, by position then member id, at most `limit`
|
|
119
|
+
* (never more than {@link MAX_COLLECTION_SIZE} + 1, so the caller can
|
|
120
|
+
* tell a full collection).
|
|
121
|
+
*/
|
|
122
|
+
readonly entries: (parentId: string, limit: number) => Effect.Effect<ReadonlyArray<CollectionEntry>, E, R>;
|
|
123
|
+
/** Replace the parent's whole order with `entries` (positions `0…n-1`), in one statement. */
|
|
124
|
+
readonly replace: (parentId: string, entries: ReadonlyArray<CollectionEntry>) => Effect.Effect<void, E, R>;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* How a family keeps its order: beside the chain (`mutable`, the default)
|
|
128
|
+
* or in a parent column, versioned with the parent (`versioned`).
|
|
129
|
+
*
|
|
130
|
+
* @since 0.1.0
|
|
131
|
+
*/
|
|
132
|
+
export type CollectionMode<E, R> = {
|
|
133
|
+
readonly kind: "mutable";
|
|
134
|
+
} | {
|
|
135
|
+
readonly kind: "versioned";
|
|
136
|
+
/**
|
|
137
|
+
* The parent column recording the order: the member ids, in order (a
|
|
138
|
+
* `jsonb` array). Keep it out of the parent's resource with
|
|
139
|
+
* {@link hideRecordedOrder} (`EDGE-001`).
|
|
140
|
+
*/
|
|
141
|
+
readonly column: string;
|
|
142
|
+
/** The parent leaf's chain store, read (ungated) by `resync` for the restored order. */
|
|
143
|
+
readonly chain: VersionChainStore<E, R>;
|
|
144
|
+
};
|
|
145
|
+
/**
|
|
146
|
+
* One family's collection: the parent and member leaves' gated operations,
|
|
147
|
+
* the store and the mode.
|
|
148
|
+
*
|
|
149
|
+
* @since 0.1.0
|
|
150
|
+
*/
|
|
151
|
+
export interface CollectionDefinition<Parent, Member, E, R> {
|
|
152
|
+
/** The family's name, traced as `${type}.<operation>`. */
|
|
153
|
+
readonly type: string;
|
|
154
|
+
/** The parent leaf: `get` gates a read, `authorize` a write, `update` records a versioned order. */
|
|
155
|
+
readonly parent: Pick<SubtypeOps<Parent, E, R>, "get" | "authorize" | "update">;
|
|
156
|
+
/** The member leaf's gated batch read. */
|
|
157
|
+
readonly members: Pick<SubtypeOps<Member, E, R>, "getMany">;
|
|
158
|
+
readonly store: CollectionStore<E, R>;
|
|
159
|
+
/** The order's model; `mutable` when omitted. */
|
|
160
|
+
readonly mode?: CollectionMode<E, R> | undefined;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* One member as a read returns it: its id, its position among the members
|
|
164
|
+
* the viewer can see (from 0), and the member resource.
|
|
165
|
+
*
|
|
166
|
+
* @since 0.1.0
|
|
167
|
+
*/
|
|
168
|
+
export interface CollectionMember<Member> {
|
|
169
|
+
readonly memberId: string;
|
|
170
|
+
readonly index: number;
|
|
171
|
+
readonly resource: Member;
|
|
172
|
+
}
|
|
173
|
+
type WriteFailure = Unauthorized | Forbidden | NodeNotFound | WriteRejected | StaleVersion | VersionConflict;
|
|
174
|
+
/**
|
|
175
|
+
* The operations {@link defineCollection} produces.
|
|
176
|
+
*
|
|
177
|
+
* @since 0.1.0
|
|
178
|
+
*/
|
|
179
|
+
export interface CollectionOps<Member, E, R> {
|
|
180
|
+
/**
|
|
181
|
+
* One page of the members the viewer can read, in order, with their
|
|
182
|
+
* total: the parent's read gate first (`NodeNotFound`), each member
|
|
183
|
+
* through its own leaf's, a hidden one omitted. The limit is clamped to
|
|
184
|
+
* `[1, MAX_PAGE_LIMIT]`.
|
|
185
|
+
*/
|
|
186
|
+
readonly list: (parentId: string, request: {
|
|
187
|
+
readonly limit: number;
|
|
188
|
+
readonly offset: number;
|
|
189
|
+
}) => Effect.Effect<Page<CollectionMember<Member>>, NodeNotFound | E, R>;
|
|
190
|
+
/**
|
|
191
|
+
* Add `memberId` at the end. Fails the parent's write gate, `NodeNotFound`
|
|
192
|
+
* for a member the writer can't read, {@link AlreadyMember} and
|
|
193
|
+
* {@link CollectionFull}; on a versioned family, the parent `update`'s
|
|
194
|
+
* failures too.
|
|
195
|
+
*/
|
|
196
|
+
readonly append: (parentId: string, memberId: string) => Effect.Effect<void, WriteFailure | AlreadyMember | CollectionFull | E, R>;
|
|
197
|
+
/**
|
|
198
|
+
* Move `memberId` to `index` (clamped to the order's bounds), shifting the
|
|
199
|
+
* members between. Fails the parent's write gate and {@link NotMember}.
|
|
200
|
+
*/
|
|
201
|
+
readonly move: (parentId: string, memberId: string, index: number) => Effect.Effect<void, WriteFailure | NotMember | E, R>;
|
|
202
|
+
/** Remove `memberId`. Fails the parent's write gate and {@link NotMember}. */
|
|
203
|
+
readonly remove: (parentId: string, memberId: string) => Effect.Effect<void, WriteFailure | NotMember | E, R>;
|
|
204
|
+
/**
|
|
205
|
+
* A versioned family's store order replaced by the order the parent's
|
|
206
|
+
* head records (after the parent's `restoreVersion`), behind the parent's
|
|
207
|
+
* write gate, keeping only the members the writer can read (as `append`
|
|
208
|
+
* checks). A mutable family, or a head recording no order, leaves the
|
|
209
|
+
* store as it is.
|
|
210
|
+
*/
|
|
211
|
+
readonly resync: (parentId: string) => Effect.Effect<void, Unauthorized | Forbidden | NodeNotFound | E, R>;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Wrap a parent leaf's `toResource` so the row it maps no longer carries a
|
|
215
|
+
* versioned collection's order `column`: the recorded order names every
|
|
216
|
+
* member, the ones the viewer can't read included, so it must never reach
|
|
217
|
+
* a reader of the parent (`EDGE-001`). The chain still records it, so
|
|
218
|
+
* {@link CollectionOps.resync} reads it back.
|
|
219
|
+
*
|
|
220
|
+
* @example
|
|
221
|
+
* ```ts
|
|
222
|
+
* import { deepStrictEqual } from "node:assert";
|
|
223
|
+
* import { hideRecordedOrder } from "@thomasfosterau/pulpit/collections";
|
|
224
|
+
*
|
|
225
|
+
* const toResource = hideRecordedOrder("memberOrder", (row) => row);
|
|
226
|
+
* const row = { id: "l-1", version: 2n, action: "update", title: "Ferns", memberOrder: ["i-1"] } as const;
|
|
227
|
+
* deepStrictEqual(toResource(row), { id: "l-1", version: 2n, action: "update", title: "Ferns" });
|
|
228
|
+
* ```
|
|
229
|
+
*
|
|
230
|
+
* @since 0.1.0
|
|
231
|
+
*/
|
|
232
|
+
export declare const hideRecordedOrder: <Resource>(column: string, toResource: (row: LeafRow) => Resource) => ((row: LeafRow) => Resource);
|
|
233
|
+
/**
|
|
234
|
+
* Bind one family's collection operations. Every operation is traced as
|
|
235
|
+
* `${type}.<operation>`, and the writes run in the caller's transaction (a
|
|
236
|
+
* versioned family's store write and parent append commit together).
|
|
237
|
+
*
|
|
238
|
+
* @since 0.1.0
|
|
239
|
+
*/
|
|
240
|
+
export declare const defineCollection: <Parent, Member, E, R>(definition: CollectionDefinition<Parent, Member, E, R>) => CollectionOps<Member, E, R>;
|
|
241
|
+
export {};
|
|
242
|
+
//# sourceMappingURL=collections.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"collections.d.ts","sourceRoot":"","sources":["../src/collections.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* **Ordered collections** (pulpit#193) — positioned members of a parent
|
|
3
|
+
* node: a list's items, an album's media, a byline's authors, a menu's
|
|
4
|
+
* entries.
|
|
5
|
+
*
|
|
6
|
+
* The behavioural spec is website's ordered list items (a reorder appends
|
|
7
|
+
* a parent version) and in-the-dark's list entries (an updatable position
|
|
8
|
+
* beside the chain), with their tests.
|
|
9
|
+
*
|
|
10
|
+
* ## The model (owner decision on pulpit#193)
|
|
11
|
+
*
|
|
12
|
+
* Both models, opt-in per family, over **one store seam**
|
|
13
|
+
* ({@link CollectionStore}), which holds each member's position:
|
|
14
|
+
*
|
|
15
|
+
* - **`mutable`** (the default): the order is state beside the chain, like
|
|
16
|
+
* an edge. A write changes the store and nothing else.
|
|
17
|
+
* - **`versioned`**: every write also appends a parent version through the
|
|
18
|
+
* parent leaf's own `update`, recording the whole order (the member ids,
|
|
19
|
+
* in order) in a declared parent column, so `VERSION-CHAIN-001` covers it
|
|
20
|
+
* and history shows each reorder. After the parent's `restoreVersion`,
|
|
21
|
+
* {@link CollectionOps.resync} replaces the store's order with the one
|
|
22
|
+
* the restored version records, keeping only the members the writer can
|
|
23
|
+
* read, as `append` does.
|
|
24
|
+
*
|
|
25
|
+
* That column holds the id of every member, the ones a reader of the
|
|
26
|
+
* parent may not be able to read included, so it must stay out of the
|
|
27
|
+
* parent's viewer-facing resource (`EDGE-001`): wrap the parent leaf's
|
|
28
|
+
* `toResource` with {@link hideRecordedOrder}. The parent's history
|
|
29
|
+
* (`getVersion`, `compare`) still shows it, behind the write gate
|
|
30
|
+
* (`READ-009`).
|
|
31
|
+
*
|
|
32
|
+
* ## The gates
|
|
33
|
+
*
|
|
34
|
+
* - **Reads** go through the parent's read gate (`get`): a hidden parent is
|
|
35
|
+
* `NodeNotFound`, never `Forbidden` (`SEC-AUTH-ENUM-001`). Each member is
|
|
36
|
+
* read through the member leaf's own gate (`getMany`), and one the viewer
|
|
37
|
+
* can't read is omitted, from the page and from its total, as `EDGE-001`
|
|
38
|
+
* omits an edge with a hidden end.
|
|
39
|
+
* - **Writes** pass the parent's write gate (`authorize`), and a member must
|
|
40
|
+
* be one the writer can read (`NodeNotFound` otherwise).
|
|
41
|
+
* - A collection holds at most {@link MAX_COLLECTION_SIZE} members, so every
|
|
42
|
+
* read and reorder is one bounded read (`SEC-INPUT-003`).
|
|
43
|
+
*
|
|
44
|
+
* @copyright Thomas Foster
|
|
45
|
+
* @module pulpit/collections
|
|
46
|
+
* @since 0.1.0
|
|
47
|
+
*/
|
|
48
|
+
import * as Arr from "effect/Array";
|
|
49
|
+
import * as Effect from "effect/Effect";
|
|
50
|
+
import * as HashMap from "effect/HashMap";
|
|
51
|
+
import * as Option from "effect/Option";
|
|
52
|
+
import * as Order from "effect/Order";
|
|
53
|
+
import * as Predicate from "effect/Predicate";
|
|
54
|
+
import * as Schema from "effect/Schema";
|
|
55
|
+
import { byCodePoint } from "./internal/list-order.js";
|
|
56
|
+
import { MAX_PAGE_LIMIT, } from "./subtype.js";
|
|
57
|
+
import { NodeNotFound, } from "./version-chain.js";
|
|
58
|
+
/**
|
|
59
|
+
* The most members a collection holds (`SEC-INPUT-003`): every read and
|
|
60
|
+
* reorder reads the whole order once, so it is bounded.
|
|
61
|
+
*
|
|
62
|
+
* @since 0.1.0
|
|
63
|
+
*/
|
|
64
|
+
export const MAX_COLLECTION_SIZE = MAX_PAGE_LIMIT;
|
|
65
|
+
// ── Errors ────────────────────────────────────────────────────────────
|
|
66
|
+
/**
|
|
67
|
+
* The member is already in the collection: the wire's 409.
|
|
68
|
+
*
|
|
69
|
+
* @since 0.1.0
|
|
70
|
+
*/
|
|
71
|
+
export class AlreadyMember extends Schema.TaggedError()("AlreadyMember", {
|
|
72
|
+
parentId: Schema.String,
|
|
73
|
+
memberId: Schema.String,
|
|
74
|
+
}) {
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* The member is not in the collection: the wire's 404.
|
|
78
|
+
*
|
|
79
|
+
* @since 0.1.0
|
|
80
|
+
*/
|
|
81
|
+
export class NotMember extends Schema.TaggedError()("NotMember", {
|
|
82
|
+
parentId: Schema.String,
|
|
83
|
+
memberId: Schema.String,
|
|
84
|
+
}) {
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* The collection already holds {@link MAX_COLLECTION_SIZE} members: the
|
|
88
|
+
* wire's 409.
|
|
89
|
+
*
|
|
90
|
+
* @since 0.1.0
|
|
91
|
+
*/
|
|
92
|
+
export class CollectionFull extends Schema.TaggedError()("CollectionFull", {
|
|
93
|
+
parentId: Schema.String,
|
|
94
|
+
max: Schema.Finite,
|
|
95
|
+
}) {
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Wrap a parent leaf's `toResource` so the row it maps no longer carries a
|
|
99
|
+
* versioned collection's order `column`: the recorded order names every
|
|
100
|
+
* member, the ones the viewer can't read included, so it must never reach
|
|
101
|
+
* a reader of the parent (`EDGE-001`). The chain still records it, so
|
|
102
|
+
* {@link CollectionOps.resync} reads it back.
|
|
103
|
+
*
|
|
104
|
+
* @example
|
|
105
|
+
* ```ts
|
|
106
|
+
* import { deepStrictEqual } from "node:assert";
|
|
107
|
+
* import { hideRecordedOrder } from "@thomasfosterau/pulpit/collections";
|
|
108
|
+
*
|
|
109
|
+
* const toResource = hideRecordedOrder("memberOrder", (row) => row);
|
|
110
|
+
* const row = { id: "l-1", version: 2n, action: "update", title: "Ferns", memberOrder: ["i-1"] } as const;
|
|
111
|
+
* deepStrictEqual(toResource(row), { id: "l-1", version: 2n, action: "update", title: "Ferns" });
|
|
112
|
+
* ```
|
|
113
|
+
*
|
|
114
|
+
* @since 0.1.0
|
|
115
|
+
*/
|
|
116
|
+
export const hideRecordedOrder = (column, toResource) => (row) => {
|
|
117
|
+
const { [column]: _recorded, ...rest } = row;
|
|
118
|
+
return toResource({ ...rest, id: row.id, version: row.version, action: row.action });
|
|
119
|
+
};
|
|
120
|
+
/** The order with positions `0…n-1`. */
|
|
121
|
+
const renumber = (ids) => Arr.map(ids, (memberId, position) => ({ memberId, position }));
|
|
122
|
+
/** By position, then member id: the store's total order. */
|
|
123
|
+
const entryOrder = Order.combine(Order.mapInput(Order.Number, (entry) => entry.position), Order.mapInput(byCodePoint, (entry) => entry.memberId));
|
|
124
|
+
/**
|
|
125
|
+
* Bind one family's collection operations. Every operation is traced as
|
|
126
|
+
* `${type}.<operation>`, and the writes run in the caller's transaction (a
|
|
127
|
+
* versioned family's store write and parent append commit together).
|
|
128
|
+
*
|
|
129
|
+
* @since 0.1.0
|
|
130
|
+
*/
|
|
131
|
+
export const defineCollection = (definition) => {
|
|
132
|
+
const { type, parent, members, store } = definition;
|
|
133
|
+
const mode = definition.mode ?? { kind: "mutable" };
|
|
134
|
+
/** The whole order, as ids (one bounded read). */
|
|
135
|
+
const orderOf = (parentId) => Effect.map(store.entries(parentId, MAX_COLLECTION_SIZE + 1), (entries) => Arr.map(Arr.sort(entries, entryOrder), (entry) => entry.memberId));
|
|
136
|
+
/** Write the new order to the store and, on a versioned family, to the parent's chain. */
|
|
137
|
+
const commit = (parentId, ids) => Effect.gen(function* () {
|
|
138
|
+
yield* store.replace(parentId, renumber(ids));
|
|
139
|
+
if (mode.kind === "versioned") {
|
|
140
|
+
yield* parent.update({ id: parentId, patch: { [mode.column]: [...ids] } });
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
const list = Effect.fn(`${type}.list`)(function* (parentId, request) {
|
|
144
|
+
// A parent the viewer can't read is missing to them (SEC-AUTH-ENUM-001).
|
|
145
|
+
yield* parent.get(parentId);
|
|
146
|
+
const ids = Arr.take(yield* orderOf(parentId), MAX_COLLECTION_SIZE);
|
|
147
|
+
const readable = Arr.isReadonlyArrayNonEmpty(ids)
|
|
148
|
+
? yield* members.getMany(ids)
|
|
149
|
+
: HashMap.empty();
|
|
150
|
+
const shown = Arr.getSomes(Arr.map(ids, (memberId) => Option.map(HashMap.get(readable, memberId), (resource) => ({ memberId, resource }))));
|
|
151
|
+
const limit = Math.max(1, Math.min(Math.trunc(request.limit), MAX_PAGE_LIMIT));
|
|
152
|
+
const offset = Math.max(0, Math.trunc(request.offset));
|
|
153
|
+
return {
|
|
154
|
+
rows: Arr.map(shown.slice(offset, offset + limit), (member, at) => ({
|
|
155
|
+
...member,
|
|
156
|
+
index: offset + at,
|
|
157
|
+
})),
|
|
158
|
+
total: shown.length,
|
|
159
|
+
};
|
|
160
|
+
});
|
|
161
|
+
const append = Effect.fn(`${type}.append`)(function* (parentId, memberId) {
|
|
162
|
+
yield* parent.authorize(parentId);
|
|
163
|
+
const readable = yield* members.getMany([memberId]);
|
|
164
|
+
if (!HashMap.has(readable, memberId)) {
|
|
165
|
+
return yield* new NodeNotFound({ type, id: memberId });
|
|
166
|
+
}
|
|
167
|
+
const ids = yield* orderOf(parentId);
|
|
168
|
+
if (ids.includes(memberId))
|
|
169
|
+
return yield* new AlreadyMember({ parentId, memberId });
|
|
170
|
+
if (ids.length >= MAX_COLLECTION_SIZE) {
|
|
171
|
+
return yield* new CollectionFull({ parentId, max: MAX_COLLECTION_SIZE });
|
|
172
|
+
}
|
|
173
|
+
yield* commit(parentId, [...ids, memberId]);
|
|
174
|
+
});
|
|
175
|
+
const move = Effect.fn(`${type}.move`)(function* (parentId, memberId, index) {
|
|
176
|
+
yield* parent.authorize(parentId);
|
|
177
|
+
const ids = yield* orderOf(parentId);
|
|
178
|
+
if (!ids.includes(memberId))
|
|
179
|
+
return yield* new NotMember({ parentId, memberId });
|
|
180
|
+
const rest = Arr.filter(ids, (id) => id !== memberId);
|
|
181
|
+
const at = Math.max(0, Math.min(Number.isFinite(index) ? Math.trunc(index) : 0, rest.length));
|
|
182
|
+
yield* commit(parentId, [...rest.slice(0, at), memberId, ...rest.slice(at)]);
|
|
183
|
+
});
|
|
184
|
+
const remove = Effect.fn(`${type}.remove`)(function* (parentId, memberId) {
|
|
185
|
+
yield* parent.authorize(parentId);
|
|
186
|
+
const ids = yield* orderOf(parentId);
|
|
187
|
+
if (!ids.includes(memberId))
|
|
188
|
+
return yield* new NotMember({ parentId, memberId });
|
|
189
|
+
yield* commit(parentId, Arr.filter(ids, (id) => id !== memberId));
|
|
190
|
+
});
|
|
191
|
+
const resync = Effect.fn(`${type}.resync`)(function* (parentId) {
|
|
192
|
+
yield* parent.authorize(parentId);
|
|
193
|
+
if (mode.kind !== "versioned")
|
|
194
|
+
return;
|
|
195
|
+
const head = yield* mode.chain.head(parentId);
|
|
196
|
+
if (Option.isNone(head))
|
|
197
|
+
return;
|
|
198
|
+
const snapshot = yield* mode.chain.snapshot(parentId, head.value.version);
|
|
199
|
+
const recorded = Option.flatMap(snapshot, (version) => {
|
|
200
|
+
const value = version.state[mode.column] ?? null;
|
|
201
|
+
return Arr.isArray(value) ? Option.some(value) : Option.none();
|
|
202
|
+
});
|
|
203
|
+
if (Option.isNone(recorded))
|
|
204
|
+
return;
|
|
205
|
+
const ids = Arr.take(Arr.dedupe(Arr.filter(recorded.value, Predicate.isString)), MAX_COLLECTION_SIZE);
|
|
206
|
+
// Gate parity with `append`: the column is writable through the parent's
|
|
207
|
+
// own `update`, so keep only the members the writer can read.
|
|
208
|
+
const readable = Arr.isReadonlyArrayNonEmpty(ids)
|
|
209
|
+
? yield* members.getMany(ids)
|
|
210
|
+
: HashMap.empty();
|
|
211
|
+
yield* store.replace(parentId, renumber(Arr.filter(ids, (id) => HashMap.has(readable, id))));
|
|
212
|
+
});
|
|
213
|
+
return { list, append, move, remove, resync };
|
|
214
|
+
};
|
|
215
|
+
//# sourceMappingURL=collections.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"collections.js","sourceRoot":"","sources":["../src/collections.ts"],"names":[],"mappings":"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"}
|