@proveanything/smartlinks 2.0.54 → 2.0.56
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/dist/api/index.d.ts +0 -1
- package/dist/api/index.js +0 -1
- package/dist/api/realtime.d.ts +31 -88
- package/dist/api/realtime.js +239 -93
- package/dist/docs/API_SUMMARY.md +66 -29
- package/dist/docs/overview.md +2 -3
- package/dist/docs/realtime.md +118 -765
- package/dist/http.js +3 -0
- package/dist/openapi.yaml +105 -27
- package/dist/types/appManifest.d.ts +11 -0
- package/dist/types/realtime.d.ts +44 -0
- package/docs/API_SUMMARY.md +66 -29
- package/docs/overview.md +2 -3
- package/docs/realtime.md +118 -765
- package/openapi.yaml +105 -27
- package/package.json +1 -1
- package/scripts/doctor.mjs +25 -0
- package/dist/api/sequence.d.ts +0 -38
- package/dist/api/sequence.js +0 -29
- package/dist/docs/sequences.md +0 -125
- package/docs/sequences.md +0 -125
package/openapi.yaml
CHANGED
|
@@ -123,7 +123,7 @@ paths:
|
|
|
123
123
|
get:
|
|
124
124
|
tags:
|
|
125
125
|
- realtime
|
|
126
|
-
summary:
|
|
126
|
+
summary: (subscribe only).
|
|
127
127
|
operationId: realtime_getAdminToken
|
|
128
128
|
security:
|
|
129
129
|
- bearerAuth: []
|
|
@@ -13960,7 +13960,7 @@ paths:
|
|
|
13960
13960
|
get:
|
|
13961
13961
|
tags:
|
|
13962
13962
|
- realtime
|
|
13963
|
-
summary:
|
|
13963
|
+
summary: "`collection:{collectionId}:*` (subscribe; publish only for the collection's admins)."
|
|
13964
13964
|
operationId: realtime_getPublicToken
|
|
13965
13965
|
security: []
|
|
13966
13966
|
parameters:
|
|
@@ -14018,6 +14018,33 @@ paths:
|
|
|
14018
14018
|
description: Unauthorized
|
|
14019
14019
|
404:
|
|
14020
14020
|
description: Not found
|
|
14021
|
+
/public/realtime/publish:
|
|
14022
|
+
post:
|
|
14023
|
+
tags:
|
|
14024
|
+
- realtime
|
|
14025
|
+
summary: "One of your app's declared channels (manifest `realtime.channels`)."
|
|
14026
|
+
operationId: realtime_channel
|
|
14027
|
+
security: []
|
|
14028
|
+
responses:
|
|
14029
|
+
200:
|
|
14030
|
+
description: Success
|
|
14031
|
+
content:
|
|
14032
|
+
application/json:
|
|
14033
|
+
schema:
|
|
14034
|
+
type: object
|
|
14035
|
+
additionalProperties: true
|
|
14036
|
+
400:
|
|
14037
|
+
description: Bad request
|
|
14038
|
+
401:
|
|
14039
|
+
description: Unauthorized
|
|
14040
|
+
404:
|
|
14041
|
+
description: Not found
|
|
14042
|
+
requestBody:
|
|
14043
|
+
required: true
|
|
14044
|
+
content:
|
|
14045
|
+
application/json:
|
|
14046
|
+
schema:
|
|
14047
|
+
$ref: "#/components/schemas/RealtimeScope"
|
|
14021
14048
|
/public/tags/{tagId}:
|
|
14022
14049
|
get:
|
|
14023
14050
|
tags:
|
|
@@ -19618,6 +19645,9 @@ components:
|
|
|
19618
19645
|
$ref: "#/components/schemas/AppManifestExecutor"
|
|
19619
19646
|
functions:
|
|
19620
19647
|
$ref: "#/components/schemas/AppManifestFunctions"
|
|
19648
|
+
realtime:
|
|
19649
|
+
type: object
|
|
19650
|
+
additionalProperties: true
|
|
19621
19651
|
data:
|
|
19622
19652
|
$ref: "#/components/schemas/AppDataDeclaration"
|
|
19623
19653
|
headless:
|
|
@@ -29830,6 +29860,79 @@ components:
|
|
|
29830
29860
|
- nonce
|
|
29831
29861
|
- mac
|
|
29832
29862
|
- clientId
|
|
29863
|
+
RealtimeScope:
|
|
29864
|
+
type: object
|
|
29865
|
+
properties:
|
|
29866
|
+
collectionId:
|
|
29867
|
+
type: string
|
|
29868
|
+
appId:
|
|
29869
|
+
type: string
|
|
29870
|
+
required:
|
|
29871
|
+
- collectionId
|
|
29872
|
+
RealtimeMessage:
|
|
29873
|
+
type: object
|
|
29874
|
+
properties:
|
|
29875
|
+
channel:
|
|
29876
|
+
type: string
|
|
29877
|
+
event:
|
|
29878
|
+
type: string
|
|
29879
|
+
data: {}
|
|
29880
|
+
at:
|
|
29881
|
+
type: number
|
|
29882
|
+
required:
|
|
29883
|
+
- channel
|
|
29884
|
+
- event
|
|
29885
|
+
- data
|
|
29886
|
+
RealtimeChangeEvent:
|
|
29887
|
+
type: object
|
|
29888
|
+
properties:
|
|
29889
|
+
type:
|
|
29890
|
+
type: string
|
|
29891
|
+
id:
|
|
29892
|
+
type: string
|
|
29893
|
+
appId:
|
|
29894
|
+
type: string
|
|
29895
|
+
recordType:
|
|
29896
|
+
type: string
|
|
29897
|
+
productId:
|
|
29898
|
+
type: string
|
|
29899
|
+
proofId:
|
|
29900
|
+
type: string
|
|
29901
|
+
status:
|
|
29902
|
+
type: string
|
|
29903
|
+
at:
|
|
29904
|
+
type: number
|
|
29905
|
+
required:
|
|
29906
|
+
- type
|
|
29907
|
+
- at
|
|
29908
|
+
RealtimeChangeFilter:
|
|
29909
|
+
type: object
|
|
29910
|
+
properties:
|
|
29911
|
+
type:
|
|
29912
|
+
type: string
|
|
29913
|
+
id:
|
|
29914
|
+
type: string
|
|
29915
|
+
appId:
|
|
29916
|
+
type: string
|
|
29917
|
+
recordType:
|
|
29918
|
+
type: string
|
|
29919
|
+
productId:
|
|
29920
|
+
type: string
|
|
29921
|
+
proofId:
|
|
29922
|
+
type: string
|
|
29923
|
+
RealtimeChannelDeclaration:
|
|
29924
|
+
type: object
|
|
29925
|
+
properties:
|
|
29926
|
+
name:
|
|
29927
|
+
type: string
|
|
29928
|
+
subscribe:
|
|
29929
|
+
type: object
|
|
29930
|
+
additionalProperties: true
|
|
29931
|
+
publish:
|
|
29932
|
+
type: object
|
|
29933
|
+
additionalProperties: true
|
|
29934
|
+
required:
|
|
29935
|
+
- name
|
|
29833
29936
|
RealtimeChannelPattern:
|
|
29834
29937
|
type: string
|
|
29835
29938
|
ResearchFetchRequest:
|
|
@@ -30958,28 +31061,3 @@ components:
|
|
|
30958
31061
|
properties:
|
|
30959
31062
|
appId:
|
|
30960
31063
|
type: string
|
|
30961
|
-
AllocateSequenceInput:
|
|
30962
|
-
type: object
|
|
30963
|
-
properties:
|
|
30964
|
-
appId:
|
|
30965
|
-
type: string
|
|
30966
|
-
sequenceId:
|
|
30967
|
-
type: string
|
|
30968
|
-
subjectId:
|
|
30969
|
-
type: string
|
|
30970
|
-
productId:
|
|
30971
|
-
type: string
|
|
30972
|
-
required:
|
|
30973
|
-
- appId
|
|
30974
|
-
- sequenceId
|
|
30975
|
-
- subjectId
|
|
30976
|
-
AllocatedSequence:
|
|
30977
|
-
type: object
|
|
30978
|
-
properties:
|
|
30979
|
-
number:
|
|
30980
|
-
type: number
|
|
30981
|
-
isNew:
|
|
30982
|
-
type: boolean
|
|
30983
|
-
required:
|
|
30984
|
-
- number
|
|
30985
|
-
- isNew
|
package/package.json
CHANGED
package/scripts/doctor.mjs
CHANGED
|
@@ -313,6 +313,31 @@ if (meta.respectsHostTheme) {
|
|
|
313
313
|
if (embeds && !handRolled.length && !blocked.size) console.log(`${GREEN}✓${RESET} embedded widgets ${DIM}— ${embeds} via the host's EmbeddedWidget${RESET}`);
|
|
314
314
|
}
|
|
315
315
|
|
|
316
|
+
// ---- realtime: SL.realtime, never a provider library or key in the app ---------------------------
|
|
317
|
+
// SmartLinks Realtime (docs/realtime.md) hands out the stream and checks every publish; an app that talks
|
|
318
|
+
// to the provider directly bypasses the channel rules (and a provider key in a bundle is a key for all).
|
|
319
|
+
{
|
|
320
|
+
const EXT = /\.(tsx|ts|jsx|js)$/;
|
|
321
|
+
const SKIP = new Set(['node_modules', 'dist', '.git', 'public']);
|
|
322
|
+
const hitsRt = [];
|
|
323
|
+
(function walkRt(dir) {
|
|
324
|
+
let entries = [];
|
|
325
|
+
try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
326
|
+
for (const e of entries) {
|
|
327
|
+
if (e.isDirectory()) { if (!SKIP.has(e.name)) walkRt(join(dir, e.name)); }
|
|
328
|
+
else if (EXT.test(e.name)) {
|
|
329
|
+
const f = join(dir, e.name);
|
|
330
|
+
const text = readFileSync(f, 'utf8');
|
|
331
|
+
if (/from\s+['"]ably(\/[^'"]*)?['"]|require\(\s*['"]ably['"]\s*\)|ABLY_(API_)?KEY/.test(text)) hitsRt.push(f.startsWith(appDir) ? f.slice(appDir.length + 1) : f);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
})(existsSync(join(appDir, 'src')) ? join(appDir, 'src') : appDir);
|
|
335
|
+
if (hitsRt.length) {
|
|
336
|
+
console.log(`${YELLOW}⚠${RESET} realtime — talks to the realtime provider directly in ${hitsRt.slice(0, 3).join(', ')}`);
|
|
337
|
+
warnings.push(`${hitsRt.join(', ')} use${hitsRt.length === 1 ? 's' : ''} the realtime provider (Ably) directly. Use SL.realtime instead — declare channels in the manifest (realtime.channels) and use realtime.channel() / onChange() (docs/realtime.md); never put a provider key in an app.`);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
316
341
|
// ---- package.json scripts point at files that exist ------------------------------------------
|
|
317
342
|
// An upgrade that copies a script LINE but not the script FILE breaks every build at that step
|
|
318
343
|
// (e.g. `postbuild: node scripts/smartlinks-deploy.mjs` with no scripts/smartlinks-deploy.mjs).
|
package/dist/api/sequence.d.ts
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
export interface AllocateSequenceInput {
|
|
2
|
-
/** The app that owns the sequence config. */
|
|
3
|
-
appId: string;
|
|
4
|
-
/** The configured sequence id — the key of data.sequenceConfigs on the app config. */
|
|
5
|
-
sequenceId: string;
|
|
6
|
-
/**
|
|
7
|
-
* The STABLE subject identity. For a `claimSet` sequence, pass the tap's virtual-proof id
|
|
8
|
-
* `<claimSetId>-<code>` (e.g. "23-oOkf8o") for a per-wristband raffle — the code is the tag's
|
|
9
|
-
* permanent id, so re-taps of the same wristband return the same number; the number is stamped
|
|
10
|
-
* on that code's doc and surfaces in `tagData`. Pass a bare claim-set id for one number per
|
|
11
|
-
* group. Never a value that changes per tap.
|
|
12
|
-
*/
|
|
13
|
-
subjectId: string;
|
|
14
|
-
/** Optional product scope, when the sequence config is scoped per product. */
|
|
15
|
-
productId?: string;
|
|
16
|
-
}
|
|
17
|
-
export interface AllocatedSequence {
|
|
18
|
-
/** The allocated (or already-held) number. */
|
|
19
|
-
number: number;
|
|
20
|
-
/** True when this call allocated a new number; false when the subject already had one. */
|
|
21
|
-
isNew: boolean;
|
|
22
|
-
}
|
|
23
|
-
export declare namespace sequence {
|
|
24
|
-
/**
|
|
25
|
-
* Allocate (or return the existing) sequence number for a subject. Idempotent — safe to
|
|
26
|
-
* call on load (auto-enter) and on a button tap; a subject that already has a number gets
|
|
27
|
-
* it back with `isNew: false`.
|
|
28
|
-
*
|
|
29
|
-
* @example
|
|
30
|
-
* const { number, isNew } = await sequence.allocate(collectionId, {
|
|
31
|
-
* appId: 'raffle-app', sequenceId: 'raffle', subjectId: claimSetId,
|
|
32
|
-
* })
|
|
33
|
-
*
|
|
34
|
-
* @deprecated Being removed. Number things in your app's own server function with
|
|
35
|
-
* `ctx.sl.counters.next()` + `ctx.sl.appRecords.claim()` (docs/server-functions.md).
|
|
36
|
-
*/
|
|
37
|
-
function allocate(collectionId: string, input: AllocateSequenceInput): Promise<AllocatedSequence>;
|
|
38
|
-
}
|
package/dist/api/sequence.js
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
// src/api/sequence.ts
|
|
2
|
-
//
|
|
3
|
-
// Sequences — allocate a guaranteed-unique, monotonic number (raffle tickets, "Nth to
|
|
4
|
-
// claim", queue positions) and stamp it onto a record. The sequence (counter key, target,
|
|
5
|
-
// field, scope) is configured server-side in app config (data.sequenceConfigs[sequenceId]);
|
|
6
|
-
// this call supplies only the subject and is idempotent — a re-tap returns the same number.
|
|
7
|
-
// See docs/sequences.md.
|
|
8
|
-
import { post } from "../http.js";
|
|
9
|
-
export var sequence;
|
|
10
|
-
(function (sequence) {
|
|
11
|
-
const base = (collectionId) => `/public/collection/${encodeURIComponent(collectionId)}/sequence`;
|
|
12
|
-
/**
|
|
13
|
-
* Allocate (or return the existing) sequence number for a subject. Idempotent — safe to
|
|
14
|
-
* call on load (auto-enter) and on a button tap; a subject that already has a number gets
|
|
15
|
-
* it back with `isNew: false`.
|
|
16
|
-
*
|
|
17
|
-
* @example
|
|
18
|
-
* const { number, isNew } = await sequence.allocate(collectionId, {
|
|
19
|
-
* appId: 'raffle-app', sequenceId: 'raffle', subjectId: claimSetId,
|
|
20
|
-
* })
|
|
21
|
-
*
|
|
22
|
-
* @deprecated Being removed. Number things in your app's own server function with
|
|
23
|
-
* `ctx.sl.counters.next()` + `ctx.sl.appRecords.claim()` (docs/server-functions.md).
|
|
24
|
-
*/
|
|
25
|
-
async function allocate(collectionId, input) {
|
|
26
|
-
return post(`${base(collectionId)}/allocate`, input);
|
|
27
|
-
}
|
|
28
|
-
sequence.allocate = allocate;
|
|
29
|
-
})(sequence || (sequence = {}));
|
package/dist/docs/sequences.md
DELETED
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
# Sequences & claim-order allocation
|
|
2
|
-
|
|
3
|
-
> **Retiring — don't build on this.** `SL.sequence.allocate` is being removed. Number things in your
|
|
4
|
-
> app's own server function instead: `ctx.sl.counters.next()` for the next number and
|
|
5
|
-
> `ctx.sl.appRecords.claim()` to key it to a tag/person and make sure no number is ever held twice —
|
|
6
|
-
> see server-functions.md → "Uniqueness, numbering and people". (The counter here lived inside the
|
|
7
|
-
> app config doc, where an admin save could rewind it.)
|
|
8
|
-
|
|
9
|
-
> **SmartLinks SDK 2.x** (current `latest`). Install `@proveanything/smartlinks@^2`.
|
|
10
|
-
|
|
11
|
-
A **sequence** hands out a guaranteed-unique, monotonic number — `1, 2, 3, …` — and stamps it
|
|
12
|
-
onto a record. It's the primitive behind raffle tickets, "you're the Nth to claim", queue
|
|
13
|
-
positions, and limited-edition numbering: cases where you need *the next number*, exactly
|
|
14
|
-
once per subject, even with a whole room (or stadium) acting at the same instant.
|
|
15
|
-
|
|
16
|
-
## The model: allocate, then stamp — and the target is the ledger
|
|
17
|
-
|
|
18
|
-
Two steps, one call:
|
|
19
|
-
|
|
20
|
-
1. **Allocate** — get the next number from an **atomic counter**. The counter lives inside
|
|
21
|
-
your **app-config** doc at `data.sequences.<key>`, so it's scoped exactly like your other
|
|
22
|
-
app config (per collection / product / variant / batch) and needs no dedicated storage.
|
|
23
|
-
It's incremented in a single statement, so 250 simultaneous taps each get a distinct
|
|
24
|
-
number in microseconds — never two the same.
|
|
25
|
-
2. **Stamp** — write that number onto the **subject's own record** (a claim set, a proof, or
|
|
26
|
-
an app record). **That record is the ledger** — there's no separate bookkeeping table.
|
|
27
|
-
Next time you read the subject, the number is just there.
|
|
28
|
-
|
|
29
|
-
Because the target is the ledger, allocation is **idempotent**: on a re-tap we read the
|
|
30
|
-
subject first and return the number it already has, rather than allocating a second one.
|
|
31
|
-
|
|
32
|
-
## Guarantees
|
|
33
|
-
|
|
34
|
-
- **Unique + monotonic** — the counter is an atomic increment, never a read-modify-write, so
|
|
35
|
-
there are no duplicates and no lost updates under load.
|
|
36
|
-
- **Idempotent per subject** — one number per subject; a re-tap returns the same number.
|
|
37
|
-
- **Concurrency-safe at scale** — the only shared hot spot is the one counter row (Postgres
|
|
38
|
-
serializes it); the stamp is per-subject (its own record), so the model holds from 250 in a
|
|
39
|
-
room to 60,000 in a stadium. A concurrent double-tap on the *same* subject at worst skips
|
|
40
|
-
one number (a harmless gap) — never puts two numbers on one person.
|
|
41
|
-
|
|
42
|
-
## Scoping
|
|
43
|
-
|
|
44
|
-
- **Counter scope** — the app-config doc it lives on: collection-wide, or per product /
|
|
45
|
-
variant / batch. All wristbands mapped to one product? Scope the counter to that product.
|
|
46
|
-
- **`key`** — a name within that doc (e.g. `raffle:2026-cup`), so one config doc can hold
|
|
47
|
-
several independent sequences.
|
|
48
|
-
- **`start`** — the first number (default `1`); use it to reserve a block (start at `51` and
|
|
49
|
-
tell people it's `1`, or just filter `< N` at draw time).
|
|
50
|
-
|
|
51
|
-
## Idempotency subject — use a *stable* id
|
|
52
|
-
|
|
53
|
-
The number is deduped by the **subject id** you pass, so it must be the **stable** identity:
|
|
54
|
-
- **Virtual-proof id `<claimSetId>-<code>`** (e.g. `23-oOkf8o`) — the right key for a
|
|
55
|
-
per-wristband raffle. The `code` is the physical tag's permanent id, so a re-tap of the same
|
|
56
|
-
wristband resolves to the same code doc → same number. The virtual proof record is transient,
|
|
57
|
-
but the **code** inside its id is stable — that's what the sink keys on (it splits the id and
|
|
58
|
-
uses only `<claimSetId>` + `<code>`).
|
|
59
|
-
- **Bare claim-set id** — when one number per *group* is intended.
|
|
60
|
-
- The authenticated **user/contact** — if they sign in / claim.
|
|
61
|
-
|
|
62
|
-
Do **not** key on the transient part of a per-tap identity (a freshly-minted virtual proof
|
|
63
|
-
record) — key on the stable **code**, which is exactly what the `<claimSetId>-<code>` subject
|
|
64
|
-
carries.
|
|
65
|
-
|
|
66
|
-
## Where the number is stored (the sink)
|
|
67
|
-
|
|
68
|
-
The stamp target is configurable — the number lives wherever you'll read it:
|
|
69
|
-
|
|
70
|
-
| Target | Subject id | Use |
|
|
71
|
-
|---|---|---|
|
|
72
|
-
| `claimSet` | the virtual-proof id `<claimSetId>-<code>` (e.g. `23-oOkf8o`) **or** a bare `<claimSetId>` | The Firestore claim-set world. **The subject decides the exact doc:** a `<claimSetId>-<code>` id stamps the individual **code** doc (per-wristband — rides back as `tagData` on the next tap, every wristband its own number); a bare id stamps the whole **set** doc (one shared value). Claim-set ids are hyphen-free, so a hyphen unambiguously means the per-code form. |
|
|
73
|
-
| `proof` | the proof id | A minted proof (value or attestation) — when the number should travel with the proof. |
|
|
74
|
-
| `appRecord` | the record id | A structured app record — for queryable, per-app data. |
|
|
75
|
-
|
|
76
|
-
## Configure the sequence (once, server-side)
|
|
77
|
-
|
|
78
|
-
You define the sequence **in your app config**, under `data.sequenceConfigs`. This is what
|
|
79
|
-
makes the public endpoint safe: the target/field/scope are set by you, not the caller.
|
|
80
|
-
|
|
81
|
-
```jsonc
|
|
82
|
-
// app config: data.sequenceConfigs
|
|
83
|
-
{
|
|
84
|
-
"raffle": {
|
|
85
|
-
"key": "raffle:2026-cup", // the named counter (data.sequences.<key>)
|
|
86
|
-
"target": "claimSet", // claimSet | proof | appRecord — the ledger domain
|
|
87
|
-
"field": "raffleNumber", // the property written on the target (comes back in tagData)
|
|
88
|
-
"start": 1 // first number (optional, default 1)
|
|
89
|
-
// no productId → one collection-wide counter (all wristbands share the sequence)
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
> **Per-wristband raffle.** Keep `target: "claimSet"` and pass the tap's virtual-proof id
|
|
95
|
-
> (`<claimSetId>-<code>`) as the subject — the sink writes the individual tag's **code** doc, so
|
|
96
|
-
> each wristband gets its own number and it surfaces in `tagData` on the next tap. Pass a bare
|
|
97
|
-
> claim-set id instead only if you want one number for the whole group.
|
|
98
|
-
|
|
99
|
-
## Call it (the widget)
|
|
100
|
-
|
|
101
|
-
Your widget calls one bounded, public endpoint on tap. It passes only the **subject id** —
|
|
102
|
-
never the target/field:
|
|
103
|
-
|
|
104
|
-
```
|
|
105
|
-
POST /api/v1/public/collection/:collectionId/sequence/allocate
|
|
106
|
-
{ "appId": "liveWidgets", "sequenceId": "raffle", "subjectId": "23-oOkf8o" }
|
|
107
|
-
→ { "number": 42, "isNew": true } // re-tap → { "number": 42, "isNew": false }
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
- **`subjectId`** is the STABLE identity. For `claimCode` it's the virtual-proof id
|
|
111
|
-
`<claimSetId>-<code>` (e.g. `23-oOkf8o`) straight off the tap — re-taps of the same wristband
|
|
112
|
-
collapse to one number. (For `claimSet` it's the claim-set id.)
|
|
113
|
-
- **Idempotent**, so both your flows are the *same call*:
|
|
114
|
-
- **Auto:** on load, call allocate → get your number (existing or freshly minted).
|
|
115
|
-
- **Button:** click → animate → same call → "Your raffle number is 42."
|
|
116
|
-
- **Refresh:** just read the `raffleNumber` field back off the claim set (or proof) — it's the
|
|
117
|
-
ledger. Or call allocate again; you'll get the same number with `isNew: false`.
|
|
118
|
-
|
|
119
|
-
Errors: `404 SEQUENCE_NOT_FOUND` (not configured), `404 CLAIMSET_NOT_FOUND` (bad subject),
|
|
120
|
-
`400 BAD_REQUEST` (missing fields).
|
|
121
|
-
|
|
122
|
-
## Drawing the winner
|
|
123
|
-
|
|
124
|
-
No separate ledger — query the claim sets (or proofs) where `raffleNumber` is set; that field
|
|
125
|
-
is your entry list, in allocation order.
|
package/docs/sequences.md
DELETED
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
# Sequences & claim-order allocation
|
|
2
|
-
|
|
3
|
-
> **Retiring — don't build on this.** `SL.sequence.allocate` is being removed. Number things in your
|
|
4
|
-
> app's own server function instead: `ctx.sl.counters.next()` for the next number and
|
|
5
|
-
> `ctx.sl.appRecords.claim()` to key it to a tag/person and make sure no number is ever held twice —
|
|
6
|
-
> see server-functions.md → "Uniqueness, numbering and people". (The counter here lived inside the
|
|
7
|
-
> app config doc, where an admin save could rewind it.)
|
|
8
|
-
|
|
9
|
-
> **SmartLinks SDK 2.x** (current `latest`). Install `@proveanything/smartlinks@^2`.
|
|
10
|
-
|
|
11
|
-
A **sequence** hands out a guaranteed-unique, monotonic number — `1, 2, 3, …` — and stamps it
|
|
12
|
-
onto a record. It's the primitive behind raffle tickets, "you're the Nth to claim", queue
|
|
13
|
-
positions, and limited-edition numbering: cases where you need *the next number*, exactly
|
|
14
|
-
once per subject, even with a whole room (or stadium) acting at the same instant.
|
|
15
|
-
|
|
16
|
-
## The model: allocate, then stamp — and the target is the ledger
|
|
17
|
-
|
|
18
|
-
Two steps, one call:
|
|
19
|
-
|
|
20
|
-
1. **Allocate** — get the next number from an **atomic counter**. The counter lives inside
|
|
21
|
-
your **app-config** doc at `data.sequences.<key>`, so it's scoped exactly like your other
|
|
22
|
-
app config (per collection / product / variant / batch) and needs no dedicated storage.
|
|
23
|
-
It's incremented in a single statement, so 250 simultaneous taps each get a distinct
|
|
24
|
-
number in microseconds — never two the same.
|
|
25
|
-
2. **Stamp** — write that number onto the **subject's own record** (a claim set, a proof, or
|
|
26
|
-
an app record). **That record is the ledger** — there's no separate bookkeeping table.
|
|
27
|
-
Next time you read the subject, the number is just there.
|
|
28
|
-
|
|
29
|
-
Because the target is the ledger, allocation is **idempotent**: on a re-tap we read the
|
|
30
|
-
subject first and return the number it already has, rather than allocating a second one.
|
|
31
|
-
|
|
32
|
-
## Guarantees
|
|
33
|
-
|
|
34
|
-
- **Unique + monotonic** — the counter is an atomic increment, never a read-modify-write, so
|
|
35
|
-
there are no duplicates and no lost updates under load.
|
|
36
|
-
- **Idempotent per subject** — one number per subject; a re-tap returns the same number.
|
|
37
|
-
- **Concurrency-safe at scale** — the only shared hot spot is the one counter row (Postgres
|
|
38
|
-
serializes it); the stamp is per-subject (its own record), so the model holds from 250 in a
|
|
39
|
-
room to 60,000 in a stadium. A concurrent double-tap on the *same* subject at worst skips
|
|
40
|
-
one number (a harmless gap) — never puts two numbers on one person.
|
|
41
|
-
|
|
42
|
-
## Scoping
|
|
43
|
-
|
|
44
|
-
- **Counter scope** — the app-config doc it lives on: collection-wide, or per product /
|
|
45
|
-
variant / batch. All wristbands mapped to one product? Scope the counter to that product.
|
|
46
|
-
- **`key`** — a name within that doc (e.g. `raffle:2026-cup`), so one config doc can hold
|
|
47
|
-
several independent sequences.
|
|
48
|
-
- **`start`** — the first number (default `1`); use it to reserve a block (start at `51` and
|
|
49
|
-
tell people it's `1`, or just filter `< N` at draw time).
|
|
50
|
-
|
|
51
|
-
## Idempotency subject — use a *stable* id
|
|
52
|
-
|
|
53
|
-
The number is deduped by the **subject id** you pass, so it must be the **stable** identity:
|
|
54
|
-
- **Virtual-proof id `<claimSetId>-<code>`** (e.g. `23-oOkf8o`) — the right key for a
|
|
55
|
-
per-wristband raffle. The `code` is the physical tag's permanent id, so a re-tap of the same
|
|
56
|
-
wristband resolves to the same code doc → same number. The virtual proof record is transient,
|
|
57
|
-
but the **code** inside its id is stable — that's what the sink keys on (it splits the id and
|
|
58
|
-
uses only `<claimSetId>` + `<code>`).
|
|
59
|
-
- **Bare claim-set id** — when one number per *group* is intended.
|
|
60
|
-
- The authenticated **user/contact** — if they sign in / claim.
|
|
61
|
-
|
|
62
|
-
Do **not** key on the transient part of a per-tap identity (a freshly-minted virtual proof
|
|
63
|
-
record) — key on the stable **code**, which is exactly what the `<claimSetId>-<code>` subject
|
|
64
|
-
carries.
|
|
65
|
-
|
|
66
|
-
## Where the number is stored (the sink)
|
|
67
|
-
|
|
68
|
-
The stamp target is configurable — the number lives wherever you'll read it:
|
|
69
|
-
|
|
70
|
-
| Target | Subject id | Use |
|
|
71
|
-
|---|---|---|
|
|
72
|
-
| `claimSet` | the virtual-proof id `<claimSetId>-<code>` (e.g. `23-oOkf8o`) **or** a bare `<claimSetId>` | The Firestore claim-set world. **The subject decides the exact doc:** a `<claimSetId>-<code>` id stamps the individual **code** doc (per-wristband — rides back as `tagData` on the next tap, every wristband its own number); a bare id stamps the whole **set** doc (one shared value). Claim-set ids are hyphen-free, so a hyphen unambiguously means the per-code form. |
|
|
73
|
-
| `proof` | the proof id | A minted proof (value or attestation) — when the number should travel with the proof. |
|
|
74
|
-
| `appRecord` | the record id | A structured app record — for queryable, per-app data. |
|
|
75
|
-
|
|
76
|
-
## Configure the sequence (once, server-side)
|
|
77
|
-
|
|
78
|
-
You define the sequence **in your app config**, under `data.sequenceConfigs`. This is what
|
|
79
|
-
makes the public endpoint safe: the target/field/scope are set by you, not the caller.
|
|
80
|
-
|
|
81
|
-
```jsonc
|
|
82
|
-
// app config: data.sequenceConfigs
|
|
83
|
-
{
|
|
84
|
-
"raffle": {
|
|
85
|
-
"key": "raffle:2026-cup", // the named counter (data.sequences.<key>)
|
|
86
|
-
"target": "claimSet", // claimSet | proof | appRecord — the ledger domain
|
|
87
|
-
"field": "raffleNumber", // the property written on the target (comes back in tagData)
|
|
88
|
-
"start": 1 // first number (optional, default 1)
|
|
89
|
-
// no productId → one collection-wide counter (all wristbands share the sequence)
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
> **Per-wristband raffle.** Keep `target: "claimSet"` and pass the tap's virtual-proof id
|
|
95
|
-
> (`<claimSetId>-<code>`) as the subject — the sink writes the individual tag's **code** doc, so
|
|
96
|
-
> each wristband gets its own number and it surfaces in `tagData` on the next tap. Pass a bare
|
|
97
|
-
> claim-set id instead only if you want one number for the whole group.
|
|
98
|
-
|
|
99
|
-
## Call it (the widget)
|
|
100
|
-
|
|
101
|
-
Your widget calls one bounded, public endpoint on tap. It passes only the **subject id** —
|
|
102
|
-
never the target/field:
|
|
103
|
-
|
|
104
|
-
```
|
|
105
|
-
POST /api/v1/public/collection/:collectionId/sequence/allocate
|
|
106
|
-
{ "appId": "liveWidgets", "sequenceId": "raffle", "subjectId": "23-oOkf8o" }
|
|
107
|
-
→ { "number": 42, "isNew": true } // re-tap → { "number": 42, "isNew": false }
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
- **`subjectId`** is the STABLE identity. For `claimCode` it's the virtual-proof id
|
|
111
|
-
`<claimSetId>-<code>` (e.g. `23-oOkf8o`) straight off the tap — re-taps of the same wristband
|
|
112
|
-
collapse to one number. (For `claimSet` it's the claim-set id.)
|
|
113
|
-
- **Idempotent**, so both your flows are the *same call*:
|
|
114
|
-
- **Auto:** on load, call allocate → get your number (existing or freshly minted).
|
|
115
|
-
- **Button:** click → animate → same call → "Your raffle number is 42."
|
|
116
|
-
- **Refresh:** just read the `raffleNumber` field back off the claim set (or proof) — it's the
|
|
117
|
-
ledger. Or call allocate again; you'll get the same number with `isNew: false`.
|
|
118
|
-
|
|
119
|
-
Errors: `404 SEQUENCE_NOT_FOUND` (not configured), `404 CLAIMSET_NOT_FOUND` (bad subject),
|
|
120
|
-
`400 BAD_REQUEST` (missing fields).
|
|
121
|
-
|
|
122
|
-
## Drawing the winner
|
|
123
|
-
|
|
124
|
-
No separate ledger — query the claim sets (or proofs) where `raffleNumber` is set; that field
|
|
125
|
-
is your entry list, in allocation order.
|