Readme

Testing is work in progress, this still has bugs!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
import { sleep } from "https://esm.town/v/stevekrouse/sleep?v=1";
import { debug } from "https://esm.town/v/karfau/debug";
import { searchParams } from "https://esm.town/v/stevekrouse/searchParams?v=9";
import { mainReference } from "https://esm.town/v/karfau/mainReference";
/**
* Tries to acquire a distributed lock for `ttl` seconds.
* - the `id` defaults to `val_town-userHandle-valName` of the calling val
* - (currently) rejects ids longer then 128 chars
* - the `ttl` defauls to 3 sec
* - rejects ttl that are not safe integers or are <= 0
* @see https://dlock.univalent.net/
* be aware that the `deadline` is in seconds, not the usual millisecond
* and that it is floored, so it is very liely up to a second shorter then the ttl
* @see https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html
* Forked from @stevekrouse.dlock
*/
export async function dlock({ id, ttl = 3 }: {
// lock id by default it is based on the name of the parent
id?: string;
// seconds
ttl?: number;
} = {}, _fetch = fetch): Promise<Lock> {
if (!id) {
id = mainReference(({ userHandle, valName }) =>
`val_town-${userHandle}-${valName}`
);
}
// https://github.com/losfair/dlock/blob/5f042396e4a42b7fcd384d188c9a3a81e973eaaa/src/index.ts#L19
if (id.length > 128) {
return Promise.reject(
new RangeError(
`id needs to be max 128 characters but was ${id.length}(${id})`,
),
);
// IDEA: (always) do a SHA512 hash from the composed or passed id, to avoid this problem
// https://examples.deno.land/hashing (needs imports from deno.land to provide types)
}
// https:github.com/losfair/dlock/blob/5f042396e4a42b7fcd384d188c9a3a81e973eaaa/src/index.ts#L53
if (!Number.isSafeInteger(ttl) || ttl <= 0) {
throw new RangeError("ttl has to be a safe integer above 0 but was " + ttl);
}
const throttleMs = ttl * 251;
let _lease = undefined;
let _deadline;
// https://github.com/losfair/dlock/blob/5f042396e4a42b7fcd384d188c9a3a81e973eaaa/src/index.ts#L48
const isExpired = (): boolean => _deadline < (Date.now() / 1000);
const release = async () =>
!isExpired() &&
void _fetch(
`https://dlock.univalent.net/lock/${id}/release?${
searchParams({ lease: _lease })
}`,
).catch(console.error);
const refresh = async () => {
if (isExpired()) {
throw new Error("lock is expired");
}
return _fetch(
`https://dlock.univalent.net/lock/${id}/aquire?${
searchParams({ lease: _lease, ttl })
}`,
).then(async (res) => {
const data = await res.json();
if (!res.ok) {
throw new Error(data.error || res.status + " " + res.statusText);
}
_deadline = data.deadline;
return _deadline;
});
};
const acquire = async (max = Date.now() + ttl * 1000): Promise<Lock> => {
try {
const lockRes = await _fetch(
`https://dlock.univalent.net/lock/${id}/aquire?${
searchParams({ lease: _lease, ttl })
}`,
);
const { lease, error, deadline } = debug(
await lockRes.json(),
id,
);
if (error && deadline) {
const deadlineMs = deadline * 1000;
if (deadlineMs >= max) {
throw new Error(error + " (and deadline > max)");
}
else {
let waitTime = deadlineMs - Date.now() + 1;
if (waitTime <= 0) {
waitTime = throttleMs;
}
await sleep(waitTime);
return acquire(max);
}
}
else if (!lockRes.ok || error) {
throw new Error(error || lockRes.status + " " + lockRes.statusText);
}
else if (lease && deadline) {
👆 This is a val. Vals are TypeScript snippets of code, written in the browser and run on our servers. Create scheduled functions, email yourself, and persist small pieces of data — all from the browser.