├── desk
├── desk.ship
├── sys.kelvin
├── desk.bill
├── sur
│ ├── spaces
│ │ ├── path.hoon
│ │ └── store.hoon
│ ├── verb.hoon
│ ├── station.hoon
│ ├── spider.hoon
│ ├── membership.hoon
│ ├── visas.hoon
│ ├── docket.hoon
│ ├── rooms-v2.hoon
│ └── tome.hoon
├── mar
│ ├── kv
│ │ ├── action.hoon
│ │ └── update.hoon
│ ├── feed
│ │ ├── action.hoon
│ │ └── update.hoon
│ ├── noun.hoon
│ ├── ship.hoon
│ ├── docket-0.hoon
│ ├── tome
│ │ └── action.hoon
│ ├── kelvin.hoon
│ ├── json.hoon
│ ├── mime.hoon
│ ├── bill.hoon
│ ├── hoon.hoon
│ └── txt.hoon
├── desk.docket-0
├── ted
│ ├── kv-poke-tunnel.hoon
│ └── feed-poke-tunnel.hoon
├── lib
│ ├── skeleton.hoon
│ ├── mip.hoon
│ ├── default-agent.hoon
│ ├── realm-lib.hoon
│ ├── verb.hoon
│ ├── tomelib.hoon
│ ├── dbug.hoon
│ ├── strand.hoon
│ ├── docket.hoon
│ ├── spaces.hoon
│ └── strandio.hoon
└── app
│ └── tome.hoon
├── pkg
├── src
│ ├── classes
│ │ ├── index.ts
│ │ ├── constants.ts
│ │ └── Tome.ts
│ ├── index.ts
│ └── types.ts
├── .gitignore
├── README.md
├── jest.config.ts
├── package.json
├── tests
│ └── Tome.test.ts
└── tsconfig.json
├── .prettierrc.js
├── SUMMARY.md
├── reference
├── managing-permissions.md
├── key-value-api.md
├── feed-+-log-api.md
└── initializing-stores.md
├── quick-start.md
└── README.md
/desk/desk.ship:
--------------------------------------------------------------------------------
1 | ~hostyv
--------------------------------------------------------------------------------
/desk/sys.kelvin:
--------------------------------------------------------------------------------
1 | [%zuse 413]
--------------------------------------------------------------------------------
/desk/desk.bill:
--------------------------------------------------------------------------------
1 | :~ %tome
2 | ==
--------------------------------------------------------------------------------
/desk/sur/spaces/path.hoon:
--------------------------------------------------------------------------------
1 | |%
2 | ::
3 | +$ name cord
4 | +$ path [ship=ship space=name]
5 | --
--------------------------------------------------------------------------------
/pkg/src/classes/index.ts:
--------------------------------------------------------------------------------
1 | export { Tome, DataStore, KeyValueStore, FeedStore, LogStore } from './Tome'
2 |
--------------------------------------------------------------------------------
/.prettierrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | trailingComma: 'es5',
3 | tabWidth: 4,
4 | semi: false,
5 | singleQuote: true,
6 | }
7 |
--------------------------------------------------------------------------------
/pkg/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './types'
2 | export * from './classes'
3 | import { Tome } from './classes/Tome'
4 | export { Tome as default, Tome }
5 |
--------------------------------------------------------------------------------
/pkg/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 |
6 | # Dependency directories
7 | node_modules
8 |
9 | # Build generated files
10 | lib
11 |
--------------------------------------------------------------------------------
/pkg/README.md:
--------------------------------------------------------------------------------
1 | ## Publishing
2 |
3 | - Delete `package-lock.json`, `node_modules`, `lib`
4 | - Bump version in `package.json`
5 | - `npm i`
6 | - `npm publish --access=public .`
7 |
--------------------------------------------------------------------------------
/pkg/src/classes/constants.ts:
--------------------------------------------------------------------------------
1 | export const tomeMark = 'tome-action'
2 | export const kvMark = 'kv-action'
3 | export const feedMark = 'feed-action'
4 |
5 | export const kvThread = 'kv-poke-tunnel'
6 | export const feedThread = 'feed-poke-tunnel'
7 |
--------------------------------------------------------------------------------
/desk/mar/kv/action.hoon:
--------------------------------------------------------------------------------
1 | /- *tome
2 | /+ tomelib
3 | |_ act=kv-action
4 | ++ grow
5 | |%
6 | ++ noun act
7 | --
8 | ++ grab
9 | |%
10 | ++ noun kv-action
11 | ++ json kv-action:dejs:tomelib
12 | --
13 | ++ grad %noun
14 | --
--------------------------------------------------------------------------------
/pkg/jest.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from '@jest/types'
2 | // Sync object
3 | const config: Config.InitialOptions = {
4 | verbose: true,
5 | transform: {
6 | '^.+\\.tsx?$': 'ts-jest',
7 | },
8 | }
9 | export default config
10 |
--------------------------------------------------------------------------------
/desk/mar/feed/action.hoon:
--------------------------------------------------------------------------------
1 | /- *tome
2 | /+ tomelib
3 | |_ act=feed-action
4 | ++ grow
5 | |%
6 | ++ noun act
7 | --
8 | ++ grab
9 | |%
10 | ++ noun feed-action
11 | ++ json feed-action:dejs:tomelib
12 | --
13 | ++ grad %noun
14 | --
--------------------------------------------------------------------------------
/desk/mar/kv/update.hoon:
--------------------------------------------------------------------------------
1 | /- *tome
2 | /+ tomelib
3 | |_ upd=kv-update
4 | ++ grab
5 | |%
6 | ++ noun kv-update
7 | --
8 | ++ grow
9 | |%
10 | ++ noun upd
11 | ++ json (kv-update:enjs:tomelib upd)
12 | --
13 | ++ grad %noun
14 | --
--------------------------------------------------------------------------------
/desk/mar/feed/update.hoon:
--------------------------------------------------------------------------------
1 | /- *tome
2 | /+ tomelib
3 | |_ upd=feed-update
4 | ++ grab
5 | |%
6 | ++ noun feed-update
7 | --
8 | ++ grow
9 | |%
10 | ++ noun upd
11 | ++ json (feed-update:enjs:tomelib upd)
12 | --
13 | ++ grad %noun
14 | --
--------------------------------------------------------------------------------
/desk/sur/verb.hoon:
--------------------------------------------------------------------------------
1 | |%
2 | +$ event
3 | $% [%on-init ~]
4 | [%on-load ~]
5 | [%on-poke =mark]
6 | [%on-watch =path]
7 | [%on-leave =path]
8 | [%on-agent =wire sign=term]
9 | [%on-arvo =wire vane=term sign=term]
10 | [%on-fail =term]
11 | ==
12 | --
--------------------------------------------------------------------------------
/SUMMARY.md:
--------------------------------------------------------------------------------
1 | # Table of contents
2 |
3 | * [TomeDB](README.md)
4 | * [Quick Start](quick-start.md)
5 |
6 | ## Reference
7 |
8 | * [Initializing Stores](reference/initializing-stores.md)
9 | * [Managing Permissions](reference/managing-permissions.md)
10 | * [Key-value API](reference/key-value-api.md)
11 | * [Feed + Log API](reference/feed-+-log-api.md)
12 |
--------------------------------------------------------------------------------
/desk/sur/station.hoon:
--------------------------------------------------------------------------------
1 | |%
2 | +$ spat (pair ship cord)
3 | ::
4 | +$ action
5 | $% [%all ~]
6 | [%host who=@p]
7 | [%space wat=what sap=spat]
8 | ==
9 | ::
10 | +$ what
11 | $? %spaces
12 | %detail
13 | %host
14 | %owners
15 | %pending
16 | %initiates
17 | %members
18 | %administrators
19 | ==
20 | --
--------------------------------------------------------------------------------
/desk/desk.docket-0:
--------------------------------------------------------------------------------
1 | :~ title+'Tome DB'
2 | info+'Composable database primitive for Urbit'
3 | color+0xce.bef0
4 | version+[1 0 4]
5 | website+'https://docs.holium.com/tomedb/'
6 | license+'MIT'
7 | glob-http+['https://holium-app-globs.nyc3.cdn.digitaloceanspaces.com/realm%2Fholium-com-redirect.glob' 0v4.39m1m.t5rlt.d94cv.akdr1.389ub]
8 | base+'tome-db'
9 | ==
10 |
--------------------------------------------------------------------------------
/desk/mar/noun.hoon:
--------------------------------------------------------------------------------
1 | ::
2 | :::: /hoon/noun/mar
3 | ::
4 | /? 310
5 | !:
6 | :::: A minimal noun mark
7 | |_ non=*
8 | ++ grab |%
9 | ++ noun *
10 | --
11 | ++ grad
12 | |%
13 | ++ form %noun
14 | ++ diff |=(* +<)
15 | ++ pact |=(* +<)
16 | ++ join |=([* *] *(unit *))
17 | ++ mash |=([[ship desk *] [ship desk *]] `*`~|(%noun-mash !!))
18 | --
19 | --
20 |
--------------------------------------------------------------------------------
/desk/mar/ship.hoon:
--------------------------------------------------------------------------------
1 | |_ s=ship
2 | ++ grad %noun
3 | ++ grow
4 | |%
5 | ++ noun s
6 | ++ json s+(scot %p s)
7 | ++ mime
8 | ^- ^mime
9 | [/text/x-ship (as-octt:mimes:html (scow %p s))]
10 |
11 | --
12 | ++ grab
13 | |%
14 | ++ noun ship
15 | ++ json (su:dejs:format ;~(pfix sig fed:ag))
16 | ++ mime
17 | |= [=mite len=@ tex=@]
18 | (slav %p (snag 0 (to-wain:format tex)))
19 | --
20 | --
21 |
--------------------------------------------------------------------------------
/desk/sur/spider.hoon:
--------------------------------------------------------------------------------
1 | /+ libstrand=strand
2 | =, strand=strand:libstrand
3 | |%
4 | +$ thread $-(vase _*form:(strand ,vase))
5 | +$ input [=tid =cage]
6 | +$ tid tid:strand
7 | +$ bowl bowl:strand
8 | +$ http-error
9 | $? %bad-request :: 400
10 | %forbidden :: 403
11 | %nonexistent :: 404
12 | %offline :: 504
13 | ==
14 | +$ start-args
15 | $: parent=(unit tid)
16 | use=(unit tid)
17 | =beak
18 | file=term
19 | =vase
20 | ==
21 | --
22 |
--------------------------------------------------------------------------------
/desk/sur/membership.hoon:
--------------------------------------------------------------------------------
1 | /- *spaces-path
2 | |%
3 | ::
4 | +$ role ?(%initiate %member %admin %owner)
5 | +$ roles (set role)
6 | +$ status ?(%invited %joined %host)
7 | +$ alias cord
8 | +$ member
9 | $: =roles
10 | =alias
11 | =status
12 | ==
13 | +$ members (map ship member)
14 | +$ membership (map path members)
15 | ::
16 | +$ view
17 | $% [%membership =membership]
18 | [%members =members]
19 | [%member =member]
20 | [%is-member is-member=?]
21 | ==
22 | --
--------------------------------------------------------------------------------
/desk/mar/docket-0.hoon:
--------------------------------------------------------------------------------
1 | /+ dock=docket
2 | |_ =docket:dock
3 | ++ grow
4 | |%
5 | ++ mime
6 | ^- ^mime
7 | [/text/x-docket (as-octt:mimes:html (spit-docket:mime:dock docket))]
8 | ++ noun docket
9 | ++ json (docket:enjs:dock docket)
10 | --
11 | ++ grab
12 | |%
13 | ::
14 | ++ mime
15 | |= [=mite len=@ud tex=@]
16 | ^- docket:dock
17 | %- need
18 | %- from-clauses:mime:dock
19 | !<((list clause:dock) (slap !>(~) (ream tex)))
20 |
21 | ::
22 | ++ noun docket:dock
23 | --
24 | ++ grad %noun
25 | --
26 |
--------------------------------------------------------------------------------
/desk/mar/tome/action.hoon:
--------------------------------------------------------------------------------
1 | /- *tome
2 | |_ act=tome-action
3 | ++ grow
4 | |%
5 | ++ noun act
6 | --
7 | ++ grab
8 | |%
9 | ++ noun tome-action
10 | ++ json
11 | =, dejs:format
12 | |= jon=json
13 | ^- tome-action
14 | =* levels (su (perk [%our %space %open %unset %yes %no ~]))
15 | %. jon
16 | %- of
17 | :~ init-tome/(ot ~[ship/so space/so app/so])
18 | init-kv/(ot ~[ship/so space/so app/so bucket/so perm/(ot ~[read/levels write/levels admin/levels])])
19 | init-feed/(ot ~[ship/so space/so app/so bucket/so log/bo perm/(ot ~[read/levels write/levels admin/levels])])
20 | ==
21 | --
22 | ++ grad %noun
23 | --
--------------------------------------------------------------------------------
/desk/mar/kelvin.hoon:
--------------------------------------------------------------------------------
1 | |_ kal=waft:clay
2 | ++ grow
3 | |%
4 | ++ mime `^mime`[/text/x-kelvin (as-octs:mimes:html hoon)]
5 | ++ noun kal
6 | ++ hoon
7 | %+ rap 3
8 | %+ turn
9 | %+ sort
10 | ~(tap in (waft-to-wefts:clay kal))
11 | |= [a=weft b=weft]
12 | ?: =(lal.a lal.b)
13 | (gte num.a num.b)
14 | (gte lal.a lal.b)
15 | |= =weft
16 | (rap 3 '[%' (scot %tas lal.weft) ' ' (scot %ud num.weft) ']\0a' ~)
17 | ::
18 | ++ txt (to-wain:format hoon)
19 | --
20 | ++ grab
21 | |%
22 | ++ noun waft:clay
23 | ++ mime
24 | |= [=mite len=@ud tex=@]
25 | (cord-to-waft:clay tex)
26 | --
27 | ++ grad %noun
28 | --
29 |
--------------------------------------------------------------------------------
/desk/mar/json.hoon:
--------------------------------------------------------------------------------
1 | ::
2 | :::: /hoon/json/mar
3 | ::
4 | /? 310
5 | ::
6 | :::: compute
7 | ::
8 | =, eyre
9 | =, format
10 | =, html
11 | |_ jon=^json
12 | ::
13 | ++ grow :: convert to
14 | |%
15 | ++ mime [/application/json (as-octs:mimes -:txt)] :: convert to %mime
16 | ++ txt [(en:json jon)]~
17 | --
18 | ++ grab
19 | |% :: convert from
20 | ++ mime |=([p=mite q=octs] (fall (de:json (@t q.q)) *^json))
21 | ++ noun ^json :: clam from %noun
22 | ++ numb numb:enjs
23 | ++ time time:enjs
24 | --
25 | ++ grad %mime
26 | --
27 |
--------------------------------------------------------------------------------
/desk/mar/mime.hoon:
--------------------------------------------------------------------------------
1 | ::
2 | :::: /hoon/mime/mar
3 | ::
4 | /? 310
5 | ::
6 | |_ own=mime
7 | ++ grow
8 | ^?
9 | |%
10 | ++ jam `@`q.q.own
11 | --
12 | ::
13 | ++ grab :: convert from
14 | ^?
15 | |%
16 | ++ noun mime :: clam from %noun
17 | ++ tape
18 | |=(a=_"" [/application/x-urb-unknown (as-octt:mimes:html a)])
19 | --
20 | ++ grad
21 | ^?
22 | |%
23 | ++ form %mime
24 | ++ diff |=(mime +<)
25 | ++ pact |=(mime +<)
26 | ++ join |=([mime mime] `(unit mime)`~)
27 | ++ mash
28 | |= [[ship desk mime] [ship desk mime]]
29 | ^- mime
30 | ~|(%mime-mash !!)
31 | --
32 | --
33 |
--------------------------------------------------------------------------------
/desk/ted/kv-poke-tunnel.hoon:
--------------------------------------------------------------------------------
1 | /- spider, *tome
2 | /+ *strandio, tomelib
3 | =, strand=strand:spider
4 | |%
5 | ++ decode-input
6 | =, dejs:format
7 | |= jon=json
8 | %. jon
9 | %- ot
10 | :~ ship+so :: needs to have no sig
11 | agent+so
12 | json+so
13 | ==
14 | --
15 | ^- thread:spider
16 | |= arg=vase
17 | =/ m (strand ,vase)
18 | ^- form:m
19 | =/ jon=json
20 | (need !<((unit json) arg))
21 | =/ input (decode-input jon)
22 | =/ ship `@p`(slav %p -.input)
23 | =/ agent `@tas`-.+.input
24 | =/ act (kv-action:dejs:tomelib (need (de-json:html +.+.input)))
25 | ::
26 | :: This returns nothing on failure, 'success' on success.
27 | ;< ~ bind:m (poke [ship agent] [%kv-action !>(act)])
28 | (pure:m !>(s+'success'))
--------------------------------------------------------------------------------
/desk/ted/feed-poke-tunnel.hoon:
--------------------------------------------------------------------------------
1 | /- spider, *tome
2 | /+ *strandio, tomelib
3 | =, strand=strand:spider
4 | |%
5 | ++ decode-input
6 | =, dejs:format
7 | |= jon=json
8 | %. jon
9 | %- ot
10 | :~ ship+so :: needs to have no sig
11 | agent+so
12 | json+so
13 | ==
14 | --
15 | ^- thread:spider
16 | |= arg=vase
17 | =/ m (strand ,vase)
18 | ^- form:m
19 | =/ jon=json
20 | (need !<((unit json) arg))
21 | =/ input (decode-input jon)
22 | =/ ship `@p`(slav %p -.input)
23 | =/ agent `@tas`-.+.input
24 | =/ act (feed-action:dejs:tomelib (need (de-json:html +.+.input)))
25 | ::
26 | :: This returns nothing on failure, 'success' on success.
27 | ;< ~ bind:m (poke [ship agent] [%feed-action !>(act)])
28 | (pure:m !>(s+'success'))
--------------------------------------------------------------------------------
/desk/mar/bill.hoon:
--------------------------------------------------------------------------------
1 | |_ bil=(list dude:gall)
2 | ++ grow
3 | |%
4 | ++ mime `^mime`[/text/x-bill (as-octs:mimes:html hoon)]
5 | ++ noun bil
6 | ++ hoon
7 | ^- @t
8 | |^ (crip (of-wall:format (wrap-lines (spit-duz bil))))
9 | ::
10 | ++ wrap-lines
11 | |= taz=wall
12 | ^- wall
13 | ?~ taz ["~"]~
14 | :- (weld ":~ " i.taz)
15 | %- snoc :_ "=="
16 | (turn t.taz |=(t=tape (weld " " t)))
17 | ::
18 | ++ spit-duz
19 | |= duz=(list dude:gall)
20 | ^- wall
21 | (turn duz |=(=dude:gall ['%' (trip dude)]))
22 | --
23 | ++ txt (to-wain:format hoon)
24 | --
25 | ++ grab
26 | |%
27 | ++ noun (list dude:gall)
28 | ++ mime
29 | |= [=mite len=@ud tex=@]
30 | ~_ tex
31 | !<((list dude:gall) (slap !>(~) (ream tex)))
32 | --
33 | ++ grad %noun
34 | --
35 |
--------------------------------------------------------------------------------
/desk/lib/skeleton.hoon:
--------------------------------------------------------------------------------
1 | :: Similar to default-agent except crashes everywhere
2 | ^- agent:gall
3 | |_ bowl:gall
4 | ++ on-init
5 | ^- (quip card:agent:gall agent:gall)
6 | !!
7 | ::
8 | ++ on-save
9 | ^- vase
10 | !!
11 | ::
12 | ++ on-load
13 | |~ old-state=vase
14 | ^- (quip card:agent:gall agent:gall)
15 | !!
16 | ::
17 | ++ on-poke
18 | |~ in-poke-data=cage
19 | ^- (quip card:agent:gall agent:gall)
20 | !!
21 | ::
22 | ++ on-watch
23 | |~ path
24 | ^- (quip card:agent:gall agent:gall)
25 | !!
26 | ::
27 | ++ on-leave
28 | |~ path
29 | ^- (quip card:agent:gall agent:gall)
30 | !!
31 | ::
32 | ++ on-peek
33 | |~ path
34 | ^- (unit (unit cage))
35 | !!
36 | ::
37 | ++ on-agent
38 | |~ [wire sign:agent:gall]
39 | ^- (quip card:agent:gall agent:gall)
40 | !!
41 | ::
42 | ++ on-arvo
43 | |~ [wire =sign-arvo]
44 | ^- (quip card:agent:gall agent:gall)
45 | !!
46 | ::
47 | ++ on-fail
48 | |~ [term tang]
49 | ^- (quip card:agent:gall agent:gall)
50 | !!
51 | --
52 |
--------------------------------------------------------------------------------
/pkg/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@holium/tome-db",
3 | "version": "1.2.2",
4 | "description": "Urbit's composable database primitive",
5 | "main": "lib/index.js",
6 | "types": "lib/index.d.ts",
7 | "files": [
8 | "lib"
9 | ],
10 | "scripts": {
11 | "prepublish": "tsc",
12 | "test": "jest --forceExit"
13 | },
14 | "repository": {
15 | "type": "git",
16 | "url": "git+https://github.com/holium/tome-db.git"
17 | },
18 | "keywords": [
19 | "typescript",
20 | "urbit",
21 | "database",
22 | "tome-db"
23 | ],
24 | "author": "ajlamarc@gmail.com",
25 | "license": "MIT",
26 | "homepage": "https://github.com/holium/tome-db#readme",
27 | "devDependencies": {
28 | "@types/jest": "^29.4.0",
29 | "@types/uuid": "^9.0.0",
30 | "jest": "^29.4.3",
31 | "ts-jest": "^29.0.5",
32 | "ts-node": "^10.9.1",
33 | "ts-node-dev": "^2.0.0",
34 | "typescript": "^4.9.3"
35 | },
36 | "dependencies": {
37 | "@urbit/http-api": "^2.3.0",
38 | "uuid": "^9.0.0"
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/pkg/tests/Tome.test.ts:
--------------------------------------------------------------------------------
1 | import Tome from '../src'
2 | import Urbit from '@urbit/http-api'
3 | import { describe, expect, test, beforeAll, afterAll } from '@jest/globals'
4 |
5 | describe('basic local Tome tests', () => {
6 | test('local Tome is ours', async () => {
7 | const db = await Tome.init()
8 | expect(db.isOurStore()).toBe(true)
9 | })
10 | })
11 |
12 | describe('basic remote Tome tests', () => {
13 | let api: Urbit
14 |
15 | beforeAll(async () => {
16 | api = await Urbit.authenticate({
17 | ship: 'zod',
18 | url: 'http://localhost:8080',
19 | code: 'lidlut-tabwed-pillex-ridrup',
20 | })
21 | })
22 |
23 | test('remote Tome is ours', async () => {
24 | const db = await Tome.init(api, 'racket', {
25 | ship: 'zod',
26 | })
27 | expect(db.isOurStore()).toBe(true)
28 | })
29 |
30 | test('realm tome', async () => {
31 | const db = await Tome.init(api, 'racket', {
32 | realm: true,
33 | })
34 | expect(db.isOurStore()).toBe(true)
35 | })
36 |
37 | // this doesn't work yet
38 | afterAll(async () => {
39 | await api.delete()
40 | })
41 | })
42 |
--------------------------------------------------------------------------------
/desk/lib/mip.hoon:
--------------------------------------------------------------------------------
1 | |%
2 | ++ mip :: map of maps
3 | |$ [kex key value]
4 | (map kex (map key value))
5 | ::
6 | ++ bi :: mip engine
7 | =| a=(map * (map))
8 | |@
9 | ++ del
10 | |* [b=* c=*]
11 | =+ d=(~(gut by a) b ~)
12 | =+ e=(~(del by d) c)
13 | ?~ e
14 | (~(del by a) b)
15 | (~(put by a) b e)
16 | ::
17 | ++ get
18 | |* [b=* c=*]
19 | => .(b `_?>(?=(^ a) p.n.a)`b, c `_?>(?=(^ a) ?>(?=(^ q.n.a) p.n.q.n.a))`c)
20 | ^- (unit _?>(?=(^ a) ?>(?=(^ q.n.a) q.n.q.n.a)))
21 | (~(get by (~(gut by a) b ~)) c)
22 | ::
23 | ++ got
24 | |* [b=* c=*]
25 | (need (get b c))
26 | ::
27 | ++ gut
28 | |* [b=* c=* d=*]
29 | (~(gut by (~(gut by a) b ~)) c d)
30 | ::
31 | ++ has
32 | |* [b=* c=*]
33 | !=(~ (get b c))
34 | ::
35 | ++ key
36 | |* b=*
37 | ~(key by (~(gut by a) b ~))
38 | ::
39 | ++ put
40 | |* [b=* c=* d=*]
41 | %+ ~(put by a) b
42 | %. [c d]
43 | %~ put by
44 | (~(gut by a) b ~)
45 | ::
46 | ++ tap
47 | ::NOTE naive turn-based implementation find-errors ):
48 | =< $
49 | =+ b=`_?>(?=(^ a) *(list [x=_p.n.a _?>(?=(^ q.n.a) [y=p v=q]:n.q.n.a)]))`~
50 | |. ^+ b
51 | ?~ a
52 | b
53 | $(a r.a, b (welp (turn ~(tap by q.n.a) (lead p.n.a)) $(a l.a)))
54 | --
55 | --
56 |
--------------------------------------------------------------------------------
/desk/sur/visas.hoon:
--------------------------------------------------------------------------------
1 | /- membership, spc=spaces-path
2 | |%
3 | ::
4 | :: +$ passport
5 | :: $: =roles:membership
6 | :: alias=cord
7 | :: status=status:membership
8 | :: ==
9 | :: ::
10 | :: :: $passports: passports (access) to spaces within Realm
11 | :: +$ passports (map ship passport)
12 | :: ::
13 | :: :: $districts: subdivisions of the entire realm universe
14 | :: +$ districts (map path=path:spaces passports)
15 | ::
16 |
17 | +$ invitations (map path:spc invite)
18 | +$ invite
19 | $: inviter=ship
20 | path=path:spc
21 | role=role:membership
22 | message=cord
23 | name=name:spc
24 | type=?(%group %space %our)
25 | picture=@t
26 | color=@t
27 | invited-at=@da
28 | ==
29 | ::
30 |
31 | +$ action
32 | $% [%send-invite path=path:spc =ship =role:membership message=@t]
33 | [%accept-invite path=path:spc]
34 | [%decline-invite path=path:spc]
35 | [%invited path=path:spc =invite]
36 | [%stamped path=path:spc]
37 | [%kick-member path=path:spc =ship]
38 | [%revoke-invite path=path:spc]
39 | ==
40 |
41 | +$ reaction
42 | $% [%invite-sent path=path:spc =ship =invite =member:membership]
43 | [%invite-received path=path:spc =invite]
44 | [%invite-removed path=path:spc]
45 | [%invite-accepted path=path:spc =ship =member:membership]
46 | [%kicked path=path:spc =ship]
47 | ==
48 | ::
49 | +$ view
50 | $% [%invitations invites=invitations]
51 | ==
52 | ::
53 | --
--------------------------------------------------------------------------------
/reference/managing-permissions.md:
--------------------------------------------------------------------------------
1 | # Managing Permissions
2 |
3 | ## Types
4 |
5 | ```typescript
6 | type InviteLevel = 'read' | 'write' | 'admin' | 'block'
7 |
8 | // 'admin' allows read + write + admin
9 | // 'write' allows read + write
10 | // 'read' allows only read
11 | ```
12 |
13 | ## Methods
14 |
15 | _These can only be done by the Tome owner._
16 |
17 | ### `setPermissions`
18 |
19 | `store.setPermissions(permissions: Perm)`
20 |
21 | **Params**: the new `permissions` to set.
22 |
23 | **Returns**: `Promise`
24 |
25 | Update a store's permissions after initialization.
26 |
27 | ```typescript
28 | // store is one of: KeyValueStore, LogStore, or FeedStore
29 | await store.setPermissions({
30 | read: 'our',
31 | write: 'our'
32 | admin: 'our'
33 | })
34 | ```
35 |
36 | ### `inviteShip`
37 |
38 | `store.inviteShip(ship: string, level: InviteLevel)`
39 |
40 | **Params**: the `ship` to set permissions for and the `level` to set to.
41 |
42 | **Returns**: `Promise`
43 |
44 | Set permissions for a specific ship. Invites take precedence over bucket-level permissions.
45 |
46 | ```typescript
47 | // store is one of: KeyValueStore, LogStore, or FeedStore
48 | await store.inviteShip('timluc-miptev', 'admin')
49 | ```
50 |
51 | ### `blockShip`
52 |
53 | `store.blockShip(ship: string)`
54 |
55 | **Params**: the `ship` to block.
56 |
57 | **Returns**: `Promise`
58 |
59 | An alias for `inviteShip(ship, 'block')`.
60 |
61 | ```typescript
62 | await store.blockShip('sorreg-namtyv')
63 | ```
64 |
--------------------------------------------------------------------------------
/desk/lib/default-agent.hoon:
--------------------------------------------------------------------------------
1 | /+ skeleton
2 | |* [agent=* help=*]
3 | ?: ?=(%& help)
4 | ~| %default-agent-helpfully-crashing
5 | skeleton
6 | |_ =bowl:gall
7 | ++ on-init
8 | `agent
9 | ::
10 | ++ on-save
11 | !>(~)
12 | ::
13 | ++ on-load
14 | |= old-state=vase
15 | `agent
16 | ::
17 | ++ on-poke
18 | |= =cage
19 | ~| "unexpected poke to {} with mark {}"
20 | !!
21 | ::
22 | ++ on-watch
23 | |= =path
24 | ~| "unexpected subscription to {} on path {}"
25 | !!
26 | ::
27 | ++ on-leave
28 | |= path
29 | `agent
30 | ::
31 | ++ on-peek
32 | |= =path
33 | ~| "unexpected scry into {} on path {}"
34 | !!
35 | ::
36 | ++ on-agent
37 | |= [=wire =sign:agent:gall]
38 | ^- (quip card:agent:gall _agent)
39 | ?- -.sign
40 | %poke-ack
41 | ?~ p.sign
42 | `agent
43 | %- (slog leaf+"poke failed from {} on wire {}" u.p.sign)
44 | `agent
45 | ::
46 | %watch-ack
47 | ?~ p.sign
48 | `agent
49 | =/ =tank leaf+"subscribe failed from {} on wire {}"
50 | %- (slog tank u.p.sign)
51 | `agent
52 | ::
53 | %kick `agent
54 | %fact
55 | ~| "unexpected subscription update to {} on wire {}"
56 | ~| "with mark {}"
57 | !!
58 | ==
59 | ::
60 | ++ on-arvo
61 | |= [=wire =sign-arvo]
62 | ~| "unexpected system response {<-.sign-arvo>} to {} on wire {}"
63 | !!
64 | ::
65 | ++ on-fail
66 | |= [=term =tang]
67 | %- (slog leaf+"error in {}" >term< tang)
68 | `agent
69 | --
70 |
--------------------------------------------------------------------------------
/pkg/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */,
4 | "module": "esnext" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */,
5 | "lib": [
6 | "es5",
7 | "dom",
8 | "es2015",
9 | "es2015.collection",
10 | "ESNext"
11 | ] /* Specify library files to be included in the compilation. */,
12 | "declaration": true /* Generates corresponding '.d.ts' file. */,
13 | "declarationMap": true /* Generates a sourcemap for each corresponding '.d.ts' file. */,
14 | "sourceMap": true /* Generates corresponding '.map' file. */,
15 | "outDir": "lib" /* Redirect output structure to the directory. */,
16 | /* Strict Type-Checking Options */
17 | "strict": false /* Enable all strict type-checking options. */,
18 | /* Module Resolution Options */
19 | "moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */,
20 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
21 | /* Advanced Options */
22 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
23 | },
24 | "include": ["src/**/*.ts"],
25 | "exclude": ["node_modules", "lib"]
26 | }
27 |
--------------------------------------------------------------------------------
/desk/mar/hoon.hoon:
--------------------------------------------------------------------------------
1 | :::: /hoon/hoon/mar
2 | ::
3 | /? 310
4 | ::
5 | =, eyre
6 | |_ own=@t
7 | ::
8 | ++ grow :: convert to
9 | |%
10 | ++ mime `^mime`[/text/x-hoon (as-octs:mimes:html own)] :: convert to %mime
11 | ++ elem :: convert to %html
12 | ;div:pre(urb_codemirror "", mode "hoon"):"{(trip own)}"
13 | :: =+ gen-id="src-{<`@ui`(mug own)>}"
14 | :: ;div
15 | :: ;textarea(id "{gen-id}"):"{(trip own)}"
16 | :: ;script:"""
17 | :: CodeMirror.fromTextArea(
18 | :: window[{}],
19 | :: \{lineNumbers:true, readOnly:true}
20 | :: )
21 | :: """
22 | :: ==
23 | ++ hymn
24 | :: ;html:(head:title:"Source" "+{elem}")
25 | ;html
26 | ;head
27 | ;title:"Source"
28 | ;script@"//cdnjs.cloudflare.com/ajax/libs/codemirror/4.3.0/codemirror.js";
29 | ;script@"/lib/syntax/hoon.js";
30 | ;link(rel "stylesheet", href "//cdnjs.cloudflare.com/ajax/libs/".
31 | "codemirror/4.3.0/codemirror.min.css");
32 | ;link/"/lib/syntax/codemirror.css"(rel "stylesheet");
33 | ==
34 | ;body
35 | ;textarea#src:"{(trip own)}"
36 | ;script:'CodeMirror.fromTextArea(src, {lineNumbers:true, readOnly:true})'
37 | ==
38 | ==
39 | ++ txt
40 | (to-wain:format own)
41 | --
42 | ++ grab
43 | |% :: convert from
44 | ++ mime |=([p=mite q=octs] q.q)
45 | ++ noun @t :: clam from %noun
46 | ++ txt of-wain:format
47 | --
48 | ++ grad %txt
49 | --
50 |
--------------------------------------------------------------------------------
/desk/sur/docket.hoon:
--------------------------------------------------------------------------------
1 | |%
2 | ::
3 | +$ version
4 | [major=@ud minor=@ud patch=@ud]
5 | ::
6 | +$ glob (map path mime)
7 | ::
8 | +$ url cord
9 | :: $glob-location: How to retrieve a glob
10 | ::
11 | +$ glob-reference
12 | [hash=@uvH location=glob-location]
13 | ::
14 | +$ glob-location
15 | $% [%http =url]
16 | [%ames =ship]
17 | ==
18 | :: $href: Where a tile links to
19 | ::
20 | +$ href
21 | $% [%glob base=term =glob-reference]
22 | [%site =path]
23 | ==
24 | :: $chad: State of a docket
25 | ::
26 | +$ chad
27 | $~ [%install ~]
28 | $% :: Done
29 | [%glob =glob]
30 | [%site ~]
31 | :: Waiting
32 | [%install ~]
33 | [%suspend glob=(unit glob)]
34 | :: Error
35 | [%hung err=cord]
36 | ==
37 | ::
38 | :: $charge: A realized $docket
39 | ::
40 | +$ charge
41 | $: =docket
42 | =chad
43 | ==
44 | ::
45 | :: $clause: A key and value, as part of a docket
46 | ::
47 | :: Only used to parse $docket
48 | ::
49 | +$ clause
50 | $% [%title title=@t]
51 | [%info info=@t]
52 | [%color color=@ux]
53 | [%glob-http url=cord hash=@uvH]
54 | [%glob-ames =ship hash=@uvH]
55 | [%image =url]
56 | [%site =path]
57 | [%base base=term]
58 | [%version =version]
59 | [%website website=url]
60 | [%license license=cord]
61 | ==
62 | ::
63 | :: $docket: A description of JS bundles for a desk
64 | ::
65 | +$ docket
66 | $: %1
67 | title=@t
68 | info=@t
69 | color=@ux
70 | =href
71 | image=(unit url)
72 | =version
73 | website=url
74 | license=cord
75 | ==
76 | ::
77 | +$ charge-update
78 | $% [%initial initial=(map desk charge)]
79 | [%add-charge =desk =charge]
80 | [%del-charge =desk]
81 | ==
82 | --
83 |
--------------------------------------------------------------------------------
/desk/sur/rooms-v2.hoon:
--------------------------------------------------------------------------------
1 | ::
2 | :: %rooms-v2: is a primitive for who is currently presence.
3 | ::
4 | ::
5 | /- spaces=spaces-path
6 | |%
7 | +$ rid @t
8 | +$ title cord
9 | +$ capacity @ud
10 | +$ access ?(%public %private)
11 | +$ present (set ship)
12 | +$ whitelist (set ship)
13 | +$ space-path path:spaces
14 |
15 | +$ room
16 | $: =rid
17 | provider=ship
18 | creator=ship
19 | =access
20 | =title
21 | =present
22 | =whitelist
23 | capacity=@ud
24 | path=(unit cord)
25 | ==
26 | ::
27 | +$ rooms (map rid room)
28 | ::
29 | +$ session-state
30 | $: provider=ship
31 | current=(unit rid)
32 | =rooms
33 | ==
34 | ::
35 | +$ provider-state
36 | $: =rooms
37 | online=?
38 | banned=(set ship)
39 | ==
40 | ::
41 | +$ edit-payload
42 | $% [%title =title]
43 | [%access =access]
44 | ==
45 | ::
46 | +$ session-action
47 | $% [%set-provider =ship]
48 | [%reset-provider ~]
49 | [%create-room =rid =access =title path=(unit cord)]
50 | [%edit-room =rid =title =access]
51 | [%delete-room =rid]
52 | [%enter-room =rid]
53 | [%leave-room =rid]
54 | [%invite =rid =ship]
55 | [%kick =rid =ship]
56 | [%send-chat content=cord]
57 | ==
58 | ::
59 | +$ reaction
60 | $% [%room-entered =rid =ship]
61 | [%room-left =rid =ship]
62 | [%room-created =room]
63 | [%room-updated =room]
64 | [%room-deleted =rid]
65 | [%provider-changed provider=ship =rooms]
66 | [%invited provider=ship =rid =title =ship]
67 | [%kicked =rid =ship]
68 | [%chat-received from=ship content=cord]
69 | ==
70 | ::
71 | +$ provider-action
72 | $% [%set-online online=?]
73 | [%ban =ship]
74 | [%unban =ship]
75 | ==
76 | ::
77 | +$ signal-action
78 | $%
79 | [%signal from=ship to=ship rid=cord data=cord]
80 | ==
81 | ::
82 | +$ view
83 | $% [%session =session-state]
84 | [%room =room]
85 | [%provider =ship]
86 | ==
87 | ::
88 | --
89 |
--------------------------------------------------------------------------------
/pkg/src/types.ts:
--------------------------------------------------------------------------------
1 | import Urbit from '@urbit/http-api'
2 |
3 | type PermLevel = 'our' | 'space' | 'open' | 'unset' | 'yes' | 'no'
4 | export type InviteLevel = 'read' | 'write' | 'admin' | 'block'
5 | export type StoreType = 'kv' | 'feed'
6 |
7 | type T = string | number | boolean | object | T[]
8 | export type Value = T
9 | export type Content = T
10 |
11 | export type SubscribeUpdate = object | FeedlogEntry[] | FeedlogUpdate
12 |
13 | export interface FeedlogUpdate {
14 | type: 'new' | 'edit' | 'delete' | 'clear' | 'set-link' | 'remove-link'
15 | body: FeedlogEntry
16 | }
17 |
18 | export interface FeedlogEntry {
19 | id: string
20 | createdAt: number
21 | updatedAt: number
22 | createdBy: string
23 | updatedBy: string
24 | content: Content
25 | links: object
26 | ship?: string
27 | time?: number
28 | value?: string
29 | }
30 |
31 | export interface Perm {
32 | read: PermLevel
33 | write: PermLevel
34 | admin: PermLevel
35 | }
36 |
37 | export interface TomeOptions {
38 | realm?: boolean
39 | ship?: string
40 | space?: string
41 | agent?: string
42 | permissions?: Perm
43 | }
44 |
45 | export interface StoreOptions {
46 | bucket?: string
47 | permissions?: Perm
48 | preload?: boolean
49 | onReadyChange?: (ready: boolean) => void
50 | onLoadChange?: (loaded: boolean) => void
51 | onWriteChange?: (write: boolean) => void
52 | onAdminChange?: (admin: boolean) => void
53 | onDataChange?: (data: any) => void
54 | }
55 |
56 | export interface InitStoreOptions {
57 | api?: Urbit
58 | tomeShip?: string
59 | space?: string
60 | spaceForPath?: string
61 | app?: string
62 | agent?: string
63 | bucket?: string
64 | type?: StoreType
65 | isLog?: boolean
66 | perm?: Perm
67 | ourShip?: string
68 | locked?: boolean
69 | inRealm?: boolean
70 | preload?: boolean
71 | onReadyChange?: (ready: boolean) => void
72 | onLoadChange?: (loaded: boolean) => void
73 | onWriteChange?: (write: boolean) => void
74 | onAdminChange?: (admin: boolean) => void
75 | onDataChange?: (data: any) => void
76 | write?: boolean
77 | admin?: boolean
78 | }
79 |
--------------------------------------------------------------------------------
/reference/key-value-api.md:
--------------------------------------------------------------------------------
1 | # Key-value API
2 |
3 | ## Types
4 |
5 | ```typescript
6 | // store any valid primitive JS type.
7 | // Maintains type on retrieval.
8 | type Value = string | number | boolean | object | Value[]
9 | ```
10 |
11 | ## Methods
12 |
13 | ### `set`
14 |
15 | `kv.set(key: string, value: Value)`
16 |
17 | **Params**: a `key` and corresponding `value`.
18 |
19 | **Returns**: A promise resolving to `true` on success or `false` on failure.
20 |
21 | Set a key-value pair in the store. Overwrites existing values.
22 |
23 | ```typescript
24 | await kv.set('complex', { foo: ['bar', 3, true] })
25 | ```
26 |
27 | ### `get`
28 |
29 | `kv.get(key: string, allowCachedValue: boolean = true)`
30 |
31 | **Params**: A `key` to retrieve. If `allowCachedValue` is `true`, we will check the cache before querying Urbit.
32 |
33 | **Returns**: A promise resolving to a `Value` or `undefined` if the key does not exist.
34 |
35 | Get the value associated with a key in the store.
36 |
37 | ```typescript
38 | await kv.get('foo')
39 | ```
40 |
41 | ### `remove`
42 |
43 | `kv.remove(key: string)`
44 |
45 | **Params**: a `key` to remove.
46 |
47 | **Returns**: A promise resolving to `true` on success or `false` on failure.
48 |
49 | Remove a key-value pair from the store. If the `key` does not exist, returns `true`.
50 |
51 | ```typescript
52 | await kv.remove('foo')
53 | ```
54 |
55 | ### `clear`
56 |
57 | `kv.clear()`
58 |
59 | **Returns**: A promise resolving to `true` on success or `false` on failure.
60 |
61 | Clear all key-value pairs from the store.
62 |
63 | ```typescript
64 | await kv.clear()
65 | ```
66 |
67 | ### `all`
68 |
69 | `kv.all(useCache: boolean = false)`
70 |
71 | **Params**: If `useCache` is `true`, return the current cache instead of querying Urbit. Only relevant if `preload` was set to `false`.
72 |
73 | **Returns**: A promise resolving to a `Map>`
74 |
75 | Get all key-value pairs in the store.
76 |
77 | ```typescript
78 | await kv.all()
79 | ```
80 |
--------------------------------------------------------------------------------
/quick-start.md:
--------------------------------------------------------------------------------
1 | # Quick Start
2 |
3 | ## Client Install
4 |
5 | {% tabs %}
6 | {% tab title="Node" %}
7 | ```bash
8 | # Install via NPM
9 | npm install --save @holium/tome-db
10 |
11 | # Install via Yarn
12 | yarn add @holium/tome-db
13 | ```
14 | {% endtab %}
15 | {% endtabs %}
16 |
17 | Pair with [create-landscape-app](https://github.com/urbit/create-landscape-app) for a bootstrapped React application to build Urbit apps from.
18 |
19 | ## Urbit Setup
20 |
21 | Visit Urbit's [command-line install](https://urbit.org/getting-started/cli) page to install the binary for your system. Afterwards, boot a fresh ship:
22 |
23 | ```hoon
24 | $ ./urbit -F zod
25 | ... this can take a few minutes
26 |
27 | ~zod:dojo> |new-desk %tome
28 | ~zod:dojo> |mount %tome
29 | ```
30 |
31 | Next, you'll need to copy the Tome desk into your ship:
32 |
33 | ```bash
34 | $ git clone https://github.com/holium/tome-db
35 | $ cp -R tome-db/desk/* zod/tome/
36 | ```
37 |
38 | Finally, start the Tome back-end:
39 |
40 | ```hoon
41 | ~zod:dojo> |commit %tome
42 | ~zod:dojo> |revive %tome
43 | ```
44 |
45 | If you want to develop a spaces-enabled application for [Realm](https://www.holium.com/) (currently in private alpha), you'll also need to configure the ship with Realm's desks.
46 |
47 | ## Basic Usage
48 |
49 | ### Key-value
50 |
51 | ```typescript
52 | const db = await Tome.init(api, 'lexicon', {
53 | realm: true
54 | })
55 | const kv = await db.keyvalue({
56 | bucket: 'preferences',
57 | permissions: { read: 'open', write: 'space', admin: 'our' },
58 | preload: false,
59 | })
60 |
61 | await kv.set('foo', 'bar')
62 | await kv.set('complex', { foo: ['bar', 3, true] })
63 | await kv.all()
64 | await kv.remove('foo')
65 | await kv.get('complex')
66 | await kv.clear()
67 | ```
68 |
69 | ### Feed + Log
70 |
71 | ```typescript
72 | // ... React app boilerplate
73 | const [data, setData] = useState([])
74 |
75 | const db = await Tome.init(api, 'racket', {
76 | realm: true
77 | })
78 | // use db.log(...) for a log
79 | const feed = await db.feed({
80 | preload: true,
81 | permissions: { read: 'space', write: 'space', admin: 'our' },
82 | onDataChange: (data) => {
83 | // data comes back sorted, newest entries first.
84 | // if you want a different order, you can sort the data here.
85 | setData([...data])
86 | },
87 | })
88 |
89 | const id = await feed.post(
90 | 'https://pbs.twimg.com/media/FmHxG_UX0AACbZY?format=png&name=900x900'
91 | )
92 | await feed.edit(id, 'new-post-url')
93 | await feed.setLink(id, {reaction: ':smile:', comment: 'amazing!'})
94 | await feed.delete(id)
95 | ```
96 |
--------------------------------------------------------------------------------
/desk/sur/spaces/store.hoon:
--------------------------------------------------------------------------------
1 | :: sur/spaces/store.hoon
2 | :: Defines the types for the spaces concept.
3 | ::
4 | :: A space is a higher level concept above a %landscape group.
5 | /- membership, spaces-path, visas
6 | |%
7 | ::
8 | +$ space-path path:spaces-path
9 | +$ space-name name:spaces-path
10 | +$ space-description cord
11 | +$ group-space [creator=ship name=@tas title=@t picture=@t color=@ux]
12 | +$ token
13 | $: chain=?(%ethereum %uqbar)
14 | contract=@t
15 | symbol=@t
16 | name=@t
17 | icon=@t
18 | ==
19 | ::
20 | +$ theme-mode ?(%dark %light)
21 | +$ theme
22 | $: mode=theme-mode
23 | background-color=@t
24 | accent-color=@t
25 | input-color=@t
26 | dock-color=@t
27 | icon-color=@t
28 | text-color=@t
29 | window-color=@t
30 | wallpaper=@t
31 | ==
32 | ::
33 | +$ spaces (map space-path space)
34 | +$ space
35 | $: path=space-path
36 | name=space-name
37 | description=space-description
38 | type=space-type
39 | access=space-access
40 | picture=@t
41 | color=@t :: '#000000'
42 | =archetype
43 | theme=theme
44 | updated-at=@da
45 | ==
46 | +$ space-type ?(%group %space %our)
47 | +$ archetype ?(%home %community %creator-dao %service-dao %investment-dao)
48 | +$ space-access ?(%public %antechamber %private)
49 | ::
50 | ::
51 | :: Poke actions
52 | ::
53 | +$ action
54 | $% [%add slug=@t payload=add-payload members=members:membership]
55 | [%update path=space-path payload=edit-payload]
56 | [%remove path=space-path]
57 | [%join path=space-path]
58 | [%leave path=space-path]
59 | [%current path=space-path] :: set the currently opened space
60 | :: [%kicked path=space-path ship=ship]
61 | ==
62 | ::
63 | +$ add-payload
64 | $: name=space-name
65 | description=space-description
66 | type=space-type
67 | access=space-access
68 | picture=@t
69 | color=@t :: '#000000'
70 | =archetype
71 | ==
72 | ::
73 | +$ edit-payload
74 | $: name=@t
75 | description=@t
76 | access=space-access
77 | picture=@t
78 | color=@t
79 | =theme
80 | ==
81 | ::
82 | :: Reaction via watch paths
83 | ::
84 | +$ reaction
85 | $% [%initial =spaces =membership:membership =invitations:visas current=space-path]
86 | [%add =space members=members:membership]
87 | [%replace =space]
88 | [%remove path=space-path]
89 | [%remote-space path=space-path =space =members:membership]
90 | [%current path=space-path]
91 | ==
92 | ::
93 | :: Scry views
94 | ::
95 | +$ view :: rename to effects
96 | $% [%spaces =spaces]
97 | [%space =space]
98 | ==
99 | ::
100 | --
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ---
2 | description: Urbit's web3 and composability bridge
3 | ---
4 |
5 | # TomeDB
6 |
7 | TomeDB is an **Urbit database and JavaScript client package** with native permissioning and subscription support. With TomeDB, developers can build full-stack Urbit applications entirely in JavaScript **(no Hoon)**. Tome currently supports multiple storage types, including key-value, log, and feed.
8 |
9 | ## **Features**
10 |
11 | * _Designed for migration from preexisting dApps_: With the key-value store, TomeDB can dynamically use JavaScript local storage if an Urbit connection is not made. This means that developers can prepare and **distribute applications both on and off Urbit from a single codebase**.
12 | * _Enables app composability_: With Urbit, your users' data is theirs, forever. Applications using TomeDB can directly read and write from other data stores, with the simplicity of saving files to disk.
13 | * _Preload, caching, and callback system_: TomeDB supports both preloading and caching values directly in JavaScript, reducing the number of requests to Urbit and making it easy to deliver a snappy user experience. Callback functions are also provided to make re-rendering and state management a breeze.
14 | * _Fully integrated and modular permissions_: Developers can specify exactly which users or groups have read / write / overwrite access for each data store.
15 | * _Bootstraps Automatically_: Applications using TomeDB will create and set access to data stores on launch - no user or developer configuration required.
16 |
17 | ## Example
18 |
19 | Here’s a simple example of setting and retrieving a value with Tome and an Urbit connection:
20 |
21 | ```tsx
22 | import Urbit from '@urbit/http-api'
23 | import Tome from '@holium/tome-db'
24 |
25 | const api = new Urbit('', '', window.desk)
26 | api.ship = window.ship
27 |
28 | const db = await Tome.init(api)
29 | const store = await db.keyvalue()
30 |
31 | const result = await store.set('foo', 'bar')
32 | // result === true
33 |
34 | const value = await store.get('foo')
35 | // value === 'bar'
36 | ```
37 |
38 | ## Limitations
39 |
40 | * TomeDB currently cannot support large datasets or concurrent user counts. All data is stored and delivered by the host ship, whose max capacity is \~2-8 GB. Future plans include support for load balancing and data distribution.
41 | * Data stores are somewhat static (key-value, log, or feed). TomeDB is not a full graph or relational database replacement. It can still be useful for rapid prototyping, however.
42 |
43 | ## Quick Links
44 |
45 | * [Documentation](https://docs.holium.com/tomedb/)
46 | * [Quick start / how to install](https://docs.holium.com/tomedb/quick-start/)
47 | * [Example application](https://github.com/ajlamarc/racket)
48 | * [TomeDB + Replit demo](https://replit.com/@ajlamarc/Urbit-on-Replit?v=1)
49 | * [Source code](https://github.com/holium/tome-db)
50 | * [Report bugs or request new features](https://github.com/holium/tome-db/issues)
51 |
--------------------------------------------------------------------------------
/desk/lib/realm-lib.hoon:
--------------------------------------------------------------------------------
1 | /- s-p=spaces-path, s-s=spaces-store
2 | /- m-s=membership, v-s=visas
3 | ::
4 | |%
5 | +$ spat (pair ship cord)
6 | +$ space space:s-s
7 | +$ stype type:s-s
8 | +$ spaces spaces:s-s
9 | +$ member member:m-s
10 | +$ members members:m-s
11 | +$ invitations invitations:v-s
12 | ::
13 | ++ realms
14 | |_ $: dish=bowl:gall
15 | inis=(set ship)
16 | mems=(set ship)
17 | adms=(set ship)
18 | owns=(set ship)
19 | pend=(set ship)
20 | hust=(unit ship)
21 | sput=(unit spat)
22 | spuc=(unit space)
23 | spaz=spaces
24 | ==
25 | +* re .
26 | pat /(scot %p our.dish)/spaces/(scot %da now.dish)
27 | ++ re-abet-saz `spaces`spaz :: get spaces per query
28 | ++ re-abet-pen `(set ship)`pend :: get pending members
29 | ++ re-abet-ini `(set ship)`inis :: get initiates
30 | ++ re-abet-mem `(set ship)`mems :: get members
31 | ++ re-abet-adm `(set ship)`adms :: get administrators
32 | ++ re-abet-own `(set ship)`owns :: get owners
33 | ++ re-abet-hos `ship`(need hust) :: get host
34 | ++ re-abet-sap `spat`(need sput) :: get space-path
35 | ++ re-abet-det `space`(need spuc) :: get space details
36 | ++ all
37 | ^- spaces
38 | =- ?>(?=([%spaces *] -) +.-)
39 | .^(view:s-s %gx (welp pat /all/noun))
40 | ++ inv
41 | ^- invitations
42 | =- ?>(?=([%invitations *] -) +.-)
43 | .^(view:v-s %gx (welp pat /invitations/noun))
44 | ++ mem
45 | =+ spa=(need sput)
46 | ^- members
47 | =- ?>(?=([%members *] -) +.-)
48 | .^ view:m-s
49 | %gx
50 | (welp pat /(scot %p p.spa)/[q.spa]/members/noun)
51 | ==
52 | ++ re-read
53 | ^+ re
54 | =- re(spaz -)
55 | %- ~(rep by all)
56 | |= [sap=(pair spat space) suz=spaces]
57 | ?.(=((need hust) p.p.sap) suz (~(put by suz) sap))
58 | ++ re-abed
59 | |= [bol=bowl:gall and=(each (pair ship cord) (unit ship))]
60 | ^+ re
61 | =. dish bol
62 | ?: ?=(%.y -.and)
63 | re-team:re-read(sput `p.and, hust `p.p.and)
64 | ?~(p.and re(spaz all) re-read(hust p.and))
65 | ++ re-team
66 | ^+ re
67 | =; [i=(set @p) m=(set @p) a=(set @p) o=(set @p) p=(set @p)]
68 | re(inis i, mems m, adms a, owns o, pend p)
69 | %- ~(rep by mem)
70 | |= $: [s=ship m=member]
71 | $= o
72 | $: i=(set @p)
73 | m=(set @p)
74 | a=(set @p)
75 | o=(set @p)
76 | p=(set @p)
77 | ==
78 | ==
79 | ?: =(%invited status.m) [i.o m.o a.o o.o (~(put in p.o) s)]
80 | =: i.o ?.((~(has in roles.m) %initiate) i.o (~(put in i.o) s))
81 | m.o ?.((~(has in roles.m) %member) m.o (~(put in m.o) s))
82 | a.o ?.((~(has in roles.m) %admin) a.o (~(put in a.o) s))
83 | o.o ?.((~(has in roles.m) %owner) o.o (~(put in o.o) s))
84 | ==
85 | [i.o m.o a.o o.o p.o]
86 | --
87 | --
88 |
--------------------------------------------------------------------------------
/desk/lib/verb.hoon:
--------------------------------------------------------------------------------
1 | :: Print what your agent is doing.
2 | ::
3 | /- verb
4 | ::
5 | |= [loud=? =agent:gall]
6 | =| bowl-print=_|
7 | ^- agent:gall
8 | |^ !.
9 | |_ =bowl:gall
10 | +* this .
11 | ag ~(. agent bowl)
12 | ::
13 | ++ on-init
14 | ^- (quip card:agent:gall agent:gall)
15 | %- (print bowl |.("{}: on-init"))
16 | =^ cards agent on-init:ag
17 | [[(emit-event %on-init ~) cards] this]
18 | ::
19 | ++ on-save
20 | ^- vase
21 | %- (print bowl |.("{}: on-save"))
22 | on-save:ag
23 | ::
24 | ++ on-load
25 | |= old-state=vase
26 | ^- (quip card:agent:gall agent:gall)
27 | %- (print bowl |.("{}: on-load"))
28 | =^ cards agent (on-load:ag old-state)
29 | [[(emit-event %on-load ~) cards] this]
30 | ::
31 | ++ on-poke
32 | |= [=mark =vase]
33 | ^- (quip card:agent:gall agent:gall)
34 | %- (print bowl |.("{}: on-poke with mark {}"))
35 | ?: ?=(%verb mark)
36 | ?- !<(?(%loud %bowl) vase)
37 | %loud `this(loud !loud)
38 | %bowl `this(bowl-print !bowl-print)
39 | ==
40 | =^ cards agent (on-poke:ag mark vase)
41 | [[(emit-event %on-poke mark) cards] this]
42 | ::
43 | ++ on-watch
44 | |= =path
45 | ^- (quip card:agent:gall agent:gall)
46 | %- (print bowl |.("{}: on-watch on path {}"))
47 | =^ cards agent
48 | ?: ?=([%verb %events ~] path)
49 | [~ agent]
50 | (on-watch:ag path)
51 | [[(emit-event %on-watch path) cards] this]
52 | ::
53 | ++ on-leave
54 | |= =path
55 | ^- (quip card:agent:gall agent:gall)
56 | %- (print bowl |.("{}: on-leave on path {}"))
57 | ?: ?=([%verb %event ~] path)
58 | [~ this]
59 | =^ cards agent (on-leave:ag path)
60 | [[(emit-event %on-leave path) cards] this]
61 | ::
62 | ++ on-peek
63 | |= =path
64 | ^- (unit (unit cage))
65 | %- (print bowl |.("{}: on-peek on path {}"))
66 | (on-peek:ag path)
67 | ::
68 | ++ on-agent
69 | |= [=wire =sign:agent:gall]
70 | ^- (quip card:agent:gall agent:gall)
71 | %- (print bowl |.("{}: on-agent on wire {}, {<-.sign>}"))
72 | =^ cards agent (on-agent:ag wire sign)
73 | [[(emit-event %on-agent wire -.sign) cards] this]
74 | ::
75 | ++ on-arvo
76 | |= [=wire =sign-arvo]
77 | ^- (quip card:agent:gall agent:gall)
78 | %- %+ print bowl |.
79 | "{}: on-arvo on wire {}, {<[- +<]:sign-arvo>}"
80 | =^ cards agent (on-arvo:ag wire sign-arvo)
81 | [[(emit-event %on-arvo wire [- +<]:sign-arvo) cards] this]
82 | ::
83 | ++ on-fail
84 | |= [=term =tang]
85 | ^- (quip card:agent:gall agent:gall)
86 | %- (print bowl |.("{}: on-fail with term {}"))
87 | =^ cards agent (on-fail:ag term tang)
88 | [[(emit-event %on-fail term) cards] this]
89 | --
90 | ::
91 | ++ print
92 | |= [=bowl:gall render=(trap tape)]
93 | ^+ same
94 | =? . bowl-print
95 | %- (slog >bowl< ~)
96 | .
97 | ?. loud same
98 | %- (slog [%leaf $:render] ~)
99 | same
100 | ::
101 | ++ emit-event
102 | |= =event:verb
103 | ^- card:agent:gall
104 | [%give %fact ~[/verb/events] %verb-event !>(event)]
105 | --
106 |
--------------------------------------------------------------------------------
/desk/lib/tomelib.hoon:
--------------------------------------------------------------------------------
1 | /- *tome
2 | |%
3 | ++ dejs =, dejs:format
4 | |%
5 | ++ kv-action
6 | |= jon=json
7 | ^- ^kv-action
8 | =* perl (su (perk [%our %space %open %unset %yes %no ~]))
9 | =* invl (su (perk [%read %write %admin %block ~]))
10 | %. jon
11 | %- of
12 | :~ set-value/(ot ~[ship/so space/so app/so bucket/so key/so value/so])
13 | remove-value/(ot ~[ship/so space/so app/so bucket/so key/so])
14 | clear-kv/(ot ~[ship/so space/so app/so bucket/so])
15 | verify-kv/(ot ~[ship/so space/so app/so bucket/so])
16 | watch-kv/(ot ~[ship/so space/so app/so bucket/so])
17 | team-kv/(ot ~[ship/so space/so app/so bucket/so])
18 | perm-kv/(ot ~[ship/so space/so app/so bucket/so perm/(ot ~[read/perl write/perl admin/perl])])
19 | invite-kv/(ot ~[ship/so space/so app/so bucket/so guy/so level/invl])
20 | ==
21 | ::
22 | ++ feed-action
23 | |= jon=json
24 | ^- ^feed-action
25 | =* perl (su (perk [%our %space %open %unset %yes %no ~]))
26 | =* invl (su (perk [%read %write %admin %block ~]))
27 | %. jon
28 | %- of
29 | :~ new-post/(ot ~[ship/so space/so app/so bucket/so log/bo id/so content/so])
30 | edit-post/(ot ~[ship/so space/so app/so bucket/so log/bo id/so content/so])
31 | delete-post/(ot ~[ship/so space/so app/so bucket/so log/bo id/so])
32 | clear-feed/(ot ~[ship/so space/so app/so bucket/so log/bo])
33 | verify-feed/(ot ~[ship/so space/so app/so bucket/so log/bo])
34 | watch-feed/(ot ~[ship/so space/so app/so bucket/so log/bo])
35 | team-feed/(ot ~[ship/so space/so app/so bucket/so log/bo])
36 | perm-feed/(ot ~[ship/so space/so app/so bucket/so log/bo perm/(ot ~[read/perl write/perl admin/perl])])
37 | invite-feed/(ot ~[ship/so space/so app/so bucket/so log/bo guy/so level/invl])
38 | set-post-link/(ot ~[ship/so space/so app/so bucket/so log/bo id/so value/so])
39 | remove-post-link/(ot ~[ship/so space/so app/so bucket/so log/bo id/so])
40 | ==
41 | --
42 | ++ enjs =, enjs:format
43 | |%
44 | ++ kv-update
45 | |= upd=^kv-update
46 | ^- json
47 | ?- -.upd
48 | %set (frond key.upd s+value.upd)
49 | %remove (frond key.upd ~)
50 | %clear (pairs ~)
51 | %perm (pairs ~[[%write s+write.upd] [%admin s+admin.upd]])
52 | %get value.upd
53 | %all o+data.upd
54 | ==
55 | ::
56 | ++ feed-convert
57 | |= x=[k=@da v=feed-value]
58 | ^- json
59 | %- pairs
60 | :~ [%id s+id.v.x]
61 | [%'createdBy' s+(scot %p created-by.v.x)]
62 | [%'updatedBy' s+(scot %p updated-by.v.x)]
63 | [%'createdAt' (time created-at.v.x)]
64 | [%'updatedAt' (time updated-at.v.x)]
65 | [%content content.v.x]
66 | [%links o+links.v.x]
67 | ==
68 | ::
69 | ++ feed-update
70 | |= upd=^feed-update
71 | ^- json
72 | ?- -.upd
73 | %new (pairs ~[[%type s+'new'] [%body (pairs ~[[%id s+id.upd] [%time (time time.upd)] [%ship s+(scot %p ship.upd)] [%content s+content.upd]])]])
74 | %edit (pairs ~[[%type s+'edit'] [%body (pairs ~[[%id s+id.upd] [%time (time time.upd)] [%ship s+(scot %p ship.upd)] [%content s+content.upd]])]]) :: time is updated-time, ship is updated-by
75 | %delete (pairs ~[[%type s+'delete'] [%body (pairs ~[[%id s+id.upd] [%time (time time.upd)]])]])
76 | %clear (frond %type s+'clear')
77 | %set-link (pairs ~[[%type s+'set-link'] [%body (pairs ~[[%id s+id.upd] [%time (time time.upd)] [%ship s+ship.upd] [%value s+value.upd]])]])
78 | %remove-link (pairs ~[[%type s+'remove-link'] [%body (pairs ~[[%id s+id.upd] [%time (time time.upd)] [%ship s+ship.upd]])]])
79 | %perm (pairs ~[[%write s+write.upd] [%admin s+admin.upd]])
80 | %get
81 | ?~ post.upd ~
82 | (feed-convert [*@da post.upd])
83 | %all
84 | =/ fon ((on @da feed-value) gth) :: mop needs this to work
85 | =/ data-list (tap:fon data.upd)
86 | :- %a
87 | (turn data-list feed-convert)
88 | ==
89 | --
90 | --
--------------------------------------------------------------------------------
/desk/lib/dbug.hoon:
--------------------------------------------------------------------------------
1 | :: dbug: agent wrapper for generic debugging tools
2 | ::
3 | :: usage: %-(agent:dbug your-agent)
4 | ::
5 | |%
6 | +$ poke
7 | $% [%bowl ~]
8 | [%state grab=cord]
9 | [%incoming =about]
10 | [%outgoing =about]
11 | ==
12 | ::
13 | +$ about
14 | $@ ~
15 | $% [%ship =ship]
16 | [%path =path]
17 | [%wire =wire]
18 | [%term =term]
19 | ==
20 | ::
21 | ++ agent
22 | |= =agent:gall
23 | ^- agent:gall
24 | !.
25 | |_ =bowl:gall
26 | +* this .
27 | ag ~(. agent bowl)
28 | ::
29 | ++ on-poke
30 | |= [=mark =vase]
31 | ^- (quip card:agent:gall agent:gall)
32 | ?. ?=(%dbug mark)
33 | =^ cards agent (on-poke:ag mark vase)
34 | [cards this]
35 | =/ dbug
36 | !<(poke vase)
37 | =; =tang
38 | ((%*(. slog pri 1) tang) [~ this])
39 | ?- -.dbug
40 | %bowl [(sell !>(bowl))]~
41 | ::
42 | %state
43 | =? grab.dbug =('' grab.dbug) '-'
44 | =; product=^vase
45 | [(sell product)]~
46 | =/ state=^vase
47 | :: if the underlying app has implemented a /dbug/state scry endpoint,
48 | :: use that vase in place of +on-save's.
49 | ::
50 | =/ result=(each ^vase tang)
51 | (mule |.(q:(need (need (on-peek:ag /x/dbug/state)))))
52 | ?:(?=(%& -.result) p.result on-save:ag)
53 | %+ slap
54 | (slop state !>([bowl=bowl ..zuse]))
55 | (ream grab.dbug)
56 | ::
57 | %incoming
58 | =; =tang
59 | ?^ tang tang
60 | [%leaf "no matching subscriptions"]~
61 | %+ murn
62 | %+ sort ~(tap by sup.bowl)
63 | |= [[* a=[=ship =path]] [* b=[=ship =path]]]
64 | (aor [path ship]:a [path ship]:b)
65 | |= [=duct [=ship =path]]
66 | ^- (unit tank)
67 | =; relevant=?
68 | ?. relevant ~
69 | `>[path=path from=ship duct=duct]<
70 | ?: ?=(~ about.dbug) &
71 | ?- -.about.dbug
72 | %ship =(ship ship.about.dbug)
73 | %path ?=(^ (find path.about.dbug path))
74 | %wire %+ lien duct
75 | |=(=wire ?=(^ (find wire.about.dbug wire)))
76 | %term !!
77 | ==
78 | ::
79 | %outgoing
80 | =; =tang
81 | ?^ tang tang
82 | [%leaf "no matching subscriptions"]~
83 | %+ murn
84 | %+ sort ~(tap by wex.bowl)
85 | |= [[[a=wire *] *] [[b=wire *] *]]
86 | (aor a b)
87 | |= [[=wire =ship =term] [acked=? =path]]
88 | ^- (unit tank)
89 | =; relevant=?
90 | ?. relevant ~
91 | `>[wire=wire agnt=[ship term] path=path ackd=acked]<
92 | ?: ?=(~ about.dbug) &
93 | ?- -.about.dbug
94 | %ship =(ship ship.about.dbug)
95 | %path ?=(^ (find path.about.dbug path))
96 | %wire ?=(^ (find wire.about.dbug wire))
97 | %term =(term term.about.dbug)
98 | ==
99 | ==
100 | ::
101 | ++ on-peek
102 | |= =path
103 | ^- (unit (unit cage))
104 | ?. ?=([@ %dbug *] path)
105 | (on-peek:ag path)
106 | ?+ path [~ ~]
107 | [%u %dbug ~] ``noun+!>(&)
108 | [%x %dbug %state ~] ``noun+!>(on-save:ag)
109 | [%x %dbug %subscriptions ~] ``noun+!>([wex sup]:bowl)
110 | ==
111 | ::
112 | ++ on-init
113 | ^- (quip card:agent:gall agent:gall)
114 | =^ cards agent on-init:ag
115 | [cards this]
116 | ::
117 | ++ on-save on-save:ag
118 | ::
119 | ++ on-load
120 | |= old-state=vase
121 | ^- (quip card:agent:gall agent:gall)
122 | =^ cards agent (on-load:ag old-state)
123 | [cards this]
124 | ::
125 | ++ on-watch
126 | |= =path
127 | ^- (quip card:agent:gall agent:gall)
128 | =^ cards agent (on-watch:ag path)
129 | [cards this]
130 | ::
131 | ++ on-leave
132 | |= =path
133 | ^- (quip card:agent:gall agent:gall)
134 | =^ cards agent (on-leave:ag path)
135 | [cards this]
136 | ::
137 | ++ on-agent
138 | |= [=wire =sign:agent:gall]
139 | ^- (quip card:agent:gall agent:gall)
140 | =^ cards agent (on-agent:ag wire sign)
141 | [cards this]
142 | ::
143 | ++ on-arvo
144 | |= [=wire =sign-arvo]
145 | ^- (quip card:agent:gall agent:gall)
146 | =^ cards agent (on-arvo:ag wire sign-arvo)
147 | [cards this]
148 | ::
149 | ++ on-fail
150 | |= [=term =tang]
151 | ^- (quip card:agent:gall agent:gall)
152 | =^ cards agent (on-fail:ag term tang)
153 | [cards this]
154 | --
155 | --
156 |
--------------------------------------------------------------------------------
/reference/feed-+-log-api.md:
--------------------------------------------------------------------------------
1 | # Feed + Log API
2 |
3 | ## Types
4 |
5 | ```typescript
6 | // store any valid primitive JS type.
7 | // Maintains type on retrieval.
8 | type Content = string | number | boolean | object | Content[]
9 |
10 | interface FeedlogEntry = {
11 | id: string
12 | createdAt: number
13 | updatedAt: number
14 | createdBy: string
15 | updatedBy: string
16 | content: Content
17 | links: object // authors as keys, Content as values
18 | }
19 | ```
20 |
21 | ## Feedlog Methods
22 |
23 | ### `post`
24 |
25 | `feedlog.post(content: Content)`
26 |
27 | **Params**: `content` to post to the feedlog.
28 |
29 | **Returns**: A promise resolving to a `string` (the post ID) on success or `undefined` on failure.
30 |
31 | Add a new post to the feedlog. Automatically stores the creation time and author.
32 |
33 | ```typescript
34 | const id = await feedlog.post({ foo: ['bar', 3, true] })
35 | ```
36 |
37 | ### `edit`
38 |
39 | `feedlog.edit(id: string, newContent: Content)`
40 |
41 | **Params**: The `id` of the post to edit, and `newContent` to replace it with.
42 |
43 | **Returns**: A promise resolving to a `string` (the post ID) on success or `undefined` on failure.
44 |
45 | Edit a post in the feedlog. Automatically stores the updated time and author.
46 |
47 | ```typescript
48 | const id = await feedlog.post('original post')
49 | if (id) {
50 | await feedlog.edit(id, 'edited post')
51 | }
52 | ```
53 |
54 | ### `delete`
55 |
56 | `feedlog.delete(id: string)`
57 |
58 | **Params**: The `id` of the post to delete.
59 |
60 | **Returns**: A promise resolving to `true` on success or `false` on failure.
61 |
62 | Delete a post from the feedlog. If the post with `id` does not exist, returns `true`.
63 |
64 | ```typescript
65 | const id = await feedlog.post('original post')
66 | if (id) {
67 | await feedlog.delete(id)
68 | }
69 | ```
70 |
71 | ### `clear`
72 |
73 | `feedlog.clear()`
74 |
75 | **Returns**: A promise resolving to `true` on success or `false` on failure.
76 |
77 | Clear all posts from the feedlog.
78 |
79 | ```typescript
80 | await feedlog.clear()
81 | ```
82 |
83 | ### `get`
84 |
85 | `feedlog.get(id: string, allowCachedValue: boolean = true)`
86 |
87 | **Params**: The `id` of the post to retrieve. If `allowCachedValue` is `true`, we will check the cache before querying Urbit.
88 |
89 | **Returns**: A promise resolving to a `FeedlogEntry` or `undefined` if the post does not exist.
90 |
91 | Get the post from the feedlog with the given `id`.
92 |
93 | ```typescript
94 | await feedlog.get('foo')
95 | ```
96 |
97 | ### `all`
98 |
99 | `feedlog.all(useCache: boolean = false)`
100 |
101 | **Params**: If `useCache` is `true`, return the current cache instead of querying Urbit. Only relevant if `preload` was set to `false`.
102 |
103 | **Returns**: A promise resolving to a `FeedlogEntry[]`
104 |
105 | Retrieve all posts from the feedlog, sorted by newest first.
106 |
107 | ```typescript
108 | await feedlog.all()
109 | ```
110 |
111 | ## Feed Methods
112 |
113 | ### `setLink`
114 |
115 | `feed.setLink(id: string, content: Content)`
116 |
117 | **Params**: The `id` of the post to link to, and the `content` to associate with it.
118 |
119 | **Returns**: A promise resolving to `true` on success or `false` on failure.
120 |
121 | Associate a "link" (comment, reaction, etc.) with the feed post corresponding to `id`. Post links are stored as an object where keys are ship names and values are content. `setLink` will overwrite the link for the current ship, so call `get` first if you would like to append.
122 |
123 | ```typescript
124 | const id = await feedlog.post('original post')
125 | if (id) {
126 | await feed.setLink(id, { comment: 'first comment!', time: Date.now() })
127 | }
128 | ```
129 |
130 | ### `removeLink`
131 |
132 | `feed.removeLink(id: string)`
133 |
134 | **Params**: The `id` of the post to remove the link from.
135 |
136 | **Returns**: A promise resolving to `true` on success or `false` on failure.
137 |
138 | Remove the current ship's link to the feed post corresponding to `id`. If the post with `id` does not exist, returns true.
139 |
140 | ```typescript
141 | const id = await feedlog.post('original post')
142 | if (id) {
143 | await feed.setLink(id, { comment: 'first comment!', time: Date.now() })
144 | await feed.removeLink(id)
145 | }
146 | ```
147 |
--------------------------------------------------------------------------------
/desk/sur/tome.hoon:
--------------------------------------------------------------------------------
1 | |%
2 | +$ space @tas :: space name. if no space this is 'our'
3 | +$ app @tas :: app name (reduce namespace collisions). if no app this is 'all'
4 | +$ bucket @tas :: bucket name (with its own permissions). if no bucket this is 'def'
5 | +$ key @t :: key name
6 | +$ value @t :: value in kv store
7 | +$ content @t :: content for feed post / reply
8 | +$ id @t :: uuid for feed post
9 | +$ json-value [%s value] :: value (JSON encoded as a string). Store with %s so we aren't constantly adding it to requests.
10 | +$ ships (set ship)
11 | +$ invited (map ship invite-level)
12 | ::
13 | +$ perm-level
14 | $% %our
15 | %space
16 | %open
17 | %unset
18 | %yes
19 | %no
20 | ==
21 | ::
22 | +$ invite-level
23 | $% %read
24 | %write
25 | %admin
26 | %block
27 | ==
28 | ::
29 | +$ meta
30 | $: created-by=ship
31 | updated-by=ship
32 | created-at=time
33 | updated-at=time
34 | ==
35 | ::
36 | +$ perm [read=perm-level write=perm-level admin=perm-level]
37 | ::
38 | +$ kv-data (map key json-value)
39 | +$ kv-meta (map key meta)
40 | +$ store (map bucket [=perm invites=invited meta=kv-meta data=kv-data])
41 | ::
42 | +$ feed-ids (map id time)
43 | ::
44 | :: +$ replies ((mop time reply-value) gth)
45 | +$ links (map @t json-value)
46 | ::
47 | :: +$ reply-value
48 | :: $: created-by=ship
49 | :: updated-by=ship
50 | :: created-at=time
51 | :: updated-at=time
52 | :: content=json-value
53 | :: =links
54 | :: ==
55 | ::
56 | +$ feed-value
57 | $: =id
58 | created-by=ship
59 | updated-by=ship
60 | created-at=time
61 | updated-at=time
62 | ::
63 | content=json-value
64 | :: =replies
65 | =links
66 | ==
67 | ::
68 | +$ log ?
69 | +$ feed-data ((mop time feed-value) gth)
70 | :: if "log", %write permissions can only add, not edit or delete.
71 | :: this makes it act like a log. (admins can still edit or delete anything).
72 | +$ feed (map (pair =bucket =log) [=perm ids=feed-ids invites=invited data=feed-data])
73 | ::
74 | +$ tome-data [=store =feed]
75 | ::
76 | :: Actions and updates
77 | ::
78 | +$ tome-action
79 | $% [%init-tome ship=@t =space =app]
80 | [%init-kv ship=@t =space =app =bucket =perm]
81 | :: [%unwatch-kv ship=@t =space =app =bucket] someday...
82 | [%init-feed ship=@t =space =app =bucket =log =perm]
83 | ==
84 | ::
85 | +$ kv-action
86 | :: can be sent by any ship
87 | $% [%set-value ship=@t =space =app =bucket =key =value]
88 | [%remove-value ship=@t =space =app =bucket =key]
89 | [%clear-kv ship=@t =space =app =bucket]
90 | [%verify-kv ship=@t =space =app =bucket]
91 | :: must not be Tome owner (these are proxies for watching a foreign Tome)
92 | [%watch-kv ship=@t =space =app =bucket]
93 | [%team-kv ship=@t =space =app =bucket]
94 | :: must be Tome owner (manage permissions and invites)
95 | [%perm-kv ship=@t =space =app =bucket =perm]
96 | [%invite-kv ship=@t =space =app =bucket guy=@t level=invite-level]
97 | ==
98 | ::
99 | +$ kv-update
100 | $% [%set =key =value]
101 | [%remove =key]
102 | [%clear ~]
103 | ::
104 | [%get value=?(~ json-value)]
105 | [%all data=kv-data]
106 | [%perm write=?(%yes %no) admin=?(%yes %no)]
107 | ==
108 | ::
109 | +$ feed-action
110 | :: can be sent by any ship
111 | $% [%new-post ship=@t =space =app =bucket =log =id =content]
112 | [%edit-post ship=@t =space =app =bucket =log =id =content]
113 | [%delete-post ship=@t =space =app =bucket =log =id]
114 | [%clear-feed ship=@t =space =app =bucket =log]
115 | [%verify-feed ship=@t =space =app =bucket =log]
116 | :: must not be Tome owner (these are proxies for watching a foreign Tome)
117 | [%watch-feed ship=@t =space =app =bucket =log]
118 | [%team-feed ship=@t =space =app =bucket =log]
119 | :: must be Tome owner (manage permissions and invites)
120 | [%perm-feed ship=@t =space =app =bucket =log =perm]
121 | [%invite-feed ship=@t =space =app =bucket =log guy=@t level=invite-level]
122 | :: actions for links (anything a foreign ship wants to associate with a post)
123 | [%set-post-link ship=@t =space =app =bucket =log =id =value]
124 | [%remove-post-link ship=@t =space =app =bucket =log =id]
125 | ==
126 | ::
127 | +$ feed-update
128 | $% [%new =id =time =ship =content] :: ship is author, time is post time
129 | [%edit =id =time =ship =content] :: ship is who updated, time is update time
130 | [%delete =id =time]
131 | [%clear ~]
132 | ::
133 | [%set-link =id =time ship=@t =value] :: these are @t to make returning them easier
134 | [%remove-link =id =time ship=@t]
135 | ::
136 | [%get post=?(~ feed-value)]
137 | [%all data=feed-data]
138 | [%perm write=?(%yes %no) admin=?(%yes %no)]
139 | ==
140 | ::
141 | --
142 |
143 |
--------------------------------------------------------------------------------
/desk/lib/strand.hoon:
--------------------------------------------------------------------------------
1 | |%
2 | +$ card card:agent:gall
3 | +$ input
4 | $% [%poke =cage]
5 | [%sign =wire =sign-arvo]
6 | [%agent =wire =sign:agent:gall]
7 | [%watch =path]
8 | ==
9 | +$ strand-input [=bowl in=(unit input)]
10 | +$ tid @tatid
11 | +$ bowl
12 | $: our=ship
13 | src=ship
14 | tid=tid
15 | mom=(unit tid)
16 | wex=boat:gall
17 | sup=bitt:gall
18 | eny=@uvJ
19 | now=@da
20 | byk=beak
21 | ==
22 | ::
23 | :: cards: cards to send immediately. These will go out even if a
24 | :: later stage of the computation fails, so they shouldn't have
25 | :: any semantic effect on the rest of the system.
26 | :: Alternately, they may record an entry in contracts with
27 | :: enough information to undo the effect if the computation
28 | :: fails.
29 | :: wait: don't move on, stay here. The next sign should come back
30 | :: to this same callback.
31 | :: skip: didn't expect this input; drop it down to be handled
32 | :: elsewhere
33 | :: cont: continue computation with new callback.
34 | :: fail: abort computation; don't send effects
35 | :: done: finish computation; send effects
36 | ::
37 | ++ strand-output-raw
38 | |* a=mold
39 | $~ [~ %done *a]
40 | $: cards=(list card)
41 | $= next
42 | $% [%wait ~]
43 | [%skip ~]
44 | [%cont self=(strand-form-raw a)]
45 | [%fail err=(pair term tang)]
46 | [%done value=a]
47 | ==
48 | ==
49 | ::
50 | ++ strand-form-raw
51 | |* a=mold
52 | $-(strand-input (strand-output-raw a))
53 | ::
54 | :: Abort strand computation with error message
55 | ::
56 | ++ strand-fail
57 | |= err=(pair term tang)
58 | |= strand-input
59 | [~ %fail err]
60 | ::
61 | :: Asynchronous transcaction monad.
62 | ::
63 | :: Combo of four monads:
64 | :: - Reader on input
65 | :: - Writer on card
66 | :: - Continuation
67 | :: - Exception
68 | ::
69 | ++ strand
70 | |* a=mold
71 | |%
72 | ++ output (strand-output-raw a)
73 | ::
74 | :: Type of an strand computation.
75 | ::
76 | ++ form (strand-form-raw a)
77 | ::
78 | :: Monadic pure. Identity computation for bind.
79 | ::
80 | ++ pure
81 | |= arg=a
82 | ^- form
83 | |= strand-input
84 | [~ %done arg]
85 | ::
86 | :: Monadic bind. Combines two computations, associatively.
87 | ::
88 | ++ bind
89 | |* b=mold
90 | |= [m-b=(strand-form-raw b) fun=$-(b form)]
91 | ^- form
92 | |= input=strand-input
93 | =/ b-res=(strand-output-raw b)
94 | (m-b input)
95 | ^- output
96 | :- cards.b-res
97 | ?- -.next.b-res
98 | %wait [%wait ~]
99 | %skip [%skip ~]
100 | %cont [%cont ..$(m-b self.next.b-res)]
101 | %fail [%fail err.next.b-res]
102 | %done [%cont (fun value.next.b-res)]
103 | ==
104 | ::
105 | :: The strand monad must be evaluted in a particular way to maintain
106 | :: its monadic character. +take:eval implements this.
107 | ::
108 | ++ eval
109 | |%
110 | :: Indelible state of a strand
111 | ::
112 | +$ eval-form
113 | $: =form
114 | ==
115 | ::
116 | :: Convert initial form to eval-form
117 | ::
118 | ++ from-form
119 | |= =form
120 | ^- eval-form
121 | form
122 | ::
123 | :: The cases of results of +take
124 | ::
125 | +$ eval-result
126 | $% [%next ~]
127 | [%fail err=(pair term tang)]
128 | [%done value=a]
129 | ==
130 | ::
131 | ++ validate-mark
132 | |= [in=* =mark =bowl]
133 | ^- cage
134 | =+ .^ =dais:clay %cb
135 | /(scot %p our.bowl)/[q.byk.bowl]/(scot %da now.bowl)/[mark]
136 | ==
137 | =/ res (mule |.((vale.dais in)))
138 | ?: ?=(%| -.res)
139 | ~|(%spider-mark-fail (mean leaf+"spider: ames vale fail {}" p.res))
140 | [mark p.res]
141 | ::
142 | :: Take a new sign and run the strand against it
143 | ::
144 | ++ take
145 | :: cards: accumulate throughout recursion the cards to be
146 | :: produced now
147 | =| cards=(list card)
148 | |= [=eval-form =strand-input]
149 | ^- [[(list card) =eval-result] _eval-form]
150 | =* take-loop $
151 | =. in.strand-input
152 | ?~ in.strand-input ~
153 | =/ in u.in.strand-input
154 | ?. ?=(%agent -.in) `in
155 | ?. ?=(%fact -.sign.in) `in
156 | ::
157 | :- ~
158 | :+ %agent wire.in
159 | [%fact (validate-mark q.q.cage.sign.in p.cage.sign.in bowl.strand-input)]
160 | :: run the strand callback
161 | ::
162 | =/ =output (form.eval-form strand-input)
163 | :: add cards to cards
164 | ::
165 | =. cards
166 | %+ welp
167 | cards
168 | :: XX add tag to wires?
169 | cards.output
170 | :: case-wise handle next steps
171 | ::
172 | ?- -.next.output
173 | %wait [[cards %next ~] eval-form]
174 | %skip [[cards %next ~] eval-form]
175 | %fail [[cards %fail err.next.output] eval-form]
176 | %done [[cards %done value.next.output] eval-form]
177 | %cont
178 | :: recurse to run continuation with initialization input
179 | ::
180 | %_ take-loop
181 | form.eval-form self.next.output
182 | strand-input [bowl.strand-input ~]
183 | ==
184 | ==
185 | --
186 | --
187 | --
188 | ::
189 |
--------------------------------------------------------------------------------
/desk/lib/docket.hoon:
--------------------------------------------------------------------------------
1 | /- *docket
2 | |%
3 | ::
4 | ++ mime
5 | |%
6 | +$ draft
7 | $: title=(unit @t)
8 | info=(unit @t)
9 | color=(unit @ux)
10 | glob-http=(unit [=url hash=@uvH])
11 | glob-ames=(unit [=ship hash=@uvH])
12 | base=(unit term)
13 | site=(unit path)
14 | image=(unit url)
15 | version=(unit version)
16 | website=(unit url)
17 | license=(unit cord)
18 | ==
19 | ::
20 | ++ finalize
21 | |= =draft
22 | ^- (unit docket)
23 | ?~ title.draft ~
24 | ?~ info.draft ~
25 | ?~ color.draft ~
26 | ?~ version.draft ~
27 | ?~ website.draft ~
28 | ?~ license.draft ~
29 | =/ href=(unit href)
30 | ?^ site.draft `[%site u.site.draft]
31 | ?~ base.draft ~
32 | ?^ glob-http.draft
33 | `[%glob u.base hash.u.glob-http %http url.u.glob-http]:draft
34 | ?~ glob-ames.draft
35 | ~
36 | `[%glob u.base hash.u.glob-ames %ames ship.u.glob-ames]:draft
37 | ?~ href ~
38 | =, draft
39 | :- ~
40 | :* %1
41 | u.title
42 | u.info
43 | u.color
44 | u.href
45 | image
46 | u.version
47 | u.website
48 | u.license
49 | ==
50 | ::
51 | ++ from-clauses
52 | =| =draft
53 | |= cls=(list clause)
54 | ^- (unit docket)
55 | =* loop $
56 | ?~ cls (finalize draft)
57 | =* clause i.cls
58 | =. draft
59 | ?- -.clause
60 | %title draft(title `title.clause)
61 | %info draft(info `info.clause)
62 | %color draft(color `color.clause)
63 | %glob-http draft(glob-http `[url hash]:clause)
64 | %glob-ames draft(glob-ames `[ship hash]:clause)
65 | %base draft(base `base.clause)
66 | %site draft(site `path.clause)
67 | %image draft(image `url.clause)
68 | %version draft(version `version.clause)
69 | %website draft(website `website.clause)
70 | %license draft(license `license.clause)
71 | ==
72 | loop(cls t.cls)
73 | ::
74 | ++ to-clauses
75 | |= d=docket
76 | ^- (list clause)
77 | %- zing
78 | :~ :~ title+title.d
79 | info+info.d
80 | color+color.d
81 | version+version.d
82 | website+website.d
83 | license+license.d
84 | ==
85 | ?~ image.d ~ ~[image+u.image.d]
86 | ?: ?=(%site -.href.d) ~[site+path.href.d]
87 | =/ ref=glob-reference glob-reference.href.d
88 | :~ base+base.href.d
89 | ?- -.location.ref
90 | %http [%glob-http url.location.ref hash.ref]
91 | %ames [%glob-ames ship.location.ref hash.ref]
92 | == == ==
93 | ::
94 | ++ spit-clause
95 | |= =clause
96 | ^- tape
97 | %+ weld " {(trip -.clause)}+"
98 | ?+ -.clause "'{(trip +.clause)}'"
99 | %color (scow %ux color.clause)
100 | %site (spud path.clause)
101 | ::
102 | %glob-http
103 | "['{(trip url.clause)}' {(scow %uv hash.clause)}]"
104 | ::
105 | %glob-ames
106 | "[{(scow %p ship.clause)} {(scow %uv hash.clause)}]"
107 | ::
108 | %version
109 | =, version.clause
110 | "[{(scow %ud major)} {(scow %ud minor)} {(scow %ud patch)}]"
111 | ==
112 | ::
113 | ++ spit-docket
114 | |= dock=docket
115 | ^- tape
116 | ;: welp
117 | ":~\0a"
118 | `tape`(zing (join "\0a" (turn (to-clauses dock) spit-clause)))
119 | "\0a=="
120 | ==
121 | --
122 | ::
123 | ++ enjs
124 | =, enjs:format
125 | |%
126 | ::
127 | ++ charge-update
128 | |= u=^charge-update
129 | ^- json
130 | %+ frond -.u
131 | ^- json
132 | ?- -.u
133 | %del-charge s+desk.u
134 | ::
135 | %initial
136 | %- pairs
137 | %+ turn ~(tap by initial.u)
138 | |=([=desk c=^charge] [desk (charge c)])
139 | ::
140 | %add-charge
141 | %- pairs
142 | :~ desk+s+desk.u
143 | charge+(charge charge.u)
144 | ==
145 | ==
146 | ::
147 | ++ num
148 | |= a=@u
149 | ^- ^tape
150 | =/ p=json (numb a)
151 | ?> ?=(%n -.p)
152 | (trip p.p)
153 | ::
154 | ++ version
155 | |= v=^version
156 | ^- json
157 | :- %s
158 | %- crip
159 | "{(num major.v)}.{(num minor.v)}.{(num patch.v)}"
160 | ::
161 | ++ merge
162 | |= [a=json b=json]
163 | ^- json
164 | ?> &(?=(%o -.a) ?=(%o -.b))
165 | [%o (~(uni by p.a) p.b)]
166 | ::
167 | ++ href
168 | |= h=^href
169 | %+ frond -.h
170 | ?- -.h
171 | %site s+(spat path.h)
172 | %glob
173 | %- pairs
174 | :~ base+s+base.h
175 | glob-reference+(glob-reference glob-reference.h)
176 | ==
177 | ==
178 | ::
179 | ++ glob-reference
180 | |= ref=^glob-reference
181 | %- pairs
182 | :~ hash+s+(scot %uv hash.ref)
183 | location+(glob-location location.ref)
184 | ==
185 | ::
186 | ++ glob-location
187 | |= loc=^glob-location
188 | ^- json
189 | %+ frond -.loc
190 | ?- -.loc
191 | %http s+url.loc
192 | %ames s+(scot %p ship.loc)
193 | ==
194 | ::
195 | ++ charge
196 | |= c=^charge
197 | %+ merge (docket docket.c)
198 | %- pairs
199 | :~ chad+(chad chad.c)
200 | ==
201 | ::
202 | ++ docket
203 | |= d=^docket
204 | ^- json
205 | %- pairs
206 | :~ title+s+title.d
207 | info+s+info.d
208 | color+s+(scot %ux color.d)
209 | href+(href href.d)
210 | image+?~(image.d ~ s+u.image.d)
211 | version+(version version.d)
212 | license+s+license.d
213 | website+s+website.d
214 | ==
215 | ::
216 | ++ chad
217 | |= c=^chad
218 | %+ frond -.c
219 | ?+ -.c ~
220 | %hung s+err.c
221 | ==
222 | --
223 | --
224 |
--------------------------------------------------------------------------------
/reference/initializing-stores.md:
--------------------------------------------------------------------------------
1 | # Initializing Stores
2 |
3 | ## Permissions
4 |
5 | It's recommended to specify `permissions` on either `Tome` or its subclasses. The schema is:
6 |
7 | ```typescript
8 | interface Perm {
9 | read: 'our' | 'space' | 'open'
10 | write: 'our' | 'space' | 'open'
11 | admin: 'our' | 'space' | 'open'
12 | }
13 | ```
14 |
15 | * `our` is our ship only (`src.bowl`)
16 | * `space` is any ship in the current Holium space. If Realm is not installed, this becomes `our`.
17 | * `open` is anyone, including comets.
18 |
19 | `read` / `write` / `admin` mean slightly different things based on the store type, so they will be described below.
20 |
21 | ## `Tome`
22 |
23 | `Tome.init(api?: Urbit, app?: string, options?)`
24 |
25 | * `api`: The optional Urbit connection to be used for requests. If not set, TomeDB will attempt to use [localStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) for storing key-value pairs.
26 | * `app`: An optional app name to store under. Defaults to `'all'`. It's recommended to set this to the name of your application (be wary of collisions, though!)
27 |
28 |
29 |
30 | options
31 |
32 | Optional `ship`, `space`, `agent`, `permissions`, and `realm` flag for initializing a Tome.
33 |
34 | ```typescript
35 | options: {
36 | realm?: boolean = false
37 | ship?: string = api.ship
38 | space?: string = 'our'
39 | agent?: string = 'tome'
40 | permissions?: Perm
41 | }
42 | ```
43 |
44 | * `ship` can be specified with or without the `~`.
45 | * `agent` specifies both the desk and agent name that the JavaScript client will use. This is useful if you want to distribute a copy of TomeDB in the desk alongside your application.
46 | * If `realm` is `false` , Tome will use `ship` and `space` as specified.
47 | * If `realm` is `true`, `ship` and `space` must be either set together or not at all.
48 | * If neither are set, Tome will automatically detect and use the current Realm space and corresponding host ship, as well as handle switching application data when a user changes spaces in Realm.
49 | * To create a "locked" Tome, specify `ship` and `space` together. A locked Tome will work only in that specific space (think internal DAO tooling).
50 |
51 |
52 |
53 | * `permissions` is a default permissions level to be used by sub-classes. When creating many store instances with the same permissions, simply specify them once here.
54 |
55 |
56 |
57 | **Returns**: `Promise`
58 |
59 | All storage types must be created from a `Tome` instance, so do this first.
60 |
61 | ```typescript
62 | const api = new Urbit('', '', window.desk)
63 | api.ship = window.ship
64 |
65 | const db = await Tome.init(api, 'demo', {
66 | realm: true,
67 | ship: 'lomder-librun',
68 | space: 'Realm Forerunners',
69 | agent: 'tome',
70 | permissions: { read: 'space', write: 'space', admin: 'our' }
71 | })
72 | ```
73 |
74 | ## `Key-value`
75 |
76 | `db.keyvalue(options?)`
77 |
78 |
79 |
80 | options
81 |
82 | Optional `bucket`, `permissions`, `preload` flag, and callbacks for the key-value store.
83 |
84 | ```typescript
85 | options: {
86 | bucket?: string = 'def'
87 | preload?: boolean = true
88 | permissions?: Perm
89 | onDataChange?: (data: Map()) => void
90 | onLoadChange?: (loaded: boolean) => void
91 | onReadyChange?: (ready: boolean) => void
92 | onWriteChange?: (write: boolean) => void
93 | onAdminChange?: (admin: boolean) => void
94 | }
95 | ```
96 |
97 | * `bucket` is the bucket name to store key-value pairs under. If your app needs multiple key-value stores with different permissions, they should be different buckets. Separating buckets can also save on download sizes depending on the application.
98 | * `preload` is whether the client should fetch and cache all key-value pairs in the bucket, and subscribe to live updates. This helps with responsiveness when using an application, since most requests won't go to Urbit.
99 | * `permissions` is the permissions for the key-value store. If not set, defaults to the Tome-level permissions.
100 | * `read` can read any key-value pairs from the bucket.
101 | * `write` can create new key-value pairs or update their own values.
102 | * `admin` can create or overwrite any values in the bucket.
103 | * `onDataChange` is called whenever data in the key-value store changes, and can be used to re-render an application with new data.
104 | * `onReadyChange` is called whenever the store changes `ready` state: after initial app configuration, and whenever a user changes between spaces in Realm. Use combined with `preload` set to `false` to know when to show a loading screen, and when to start making requests.
105 | * If preload is `true`, use `onLoadChange` instead to be notified when all data has been loaded and is addressable. This also handles the case of switching between Realm spaces.
106 | * `onWriteChange` and `onAdminChange` are called when the current user's `write` and `admin` permissions have been detected to change.
107 |
108 |
109 |
110 | **Returns**: `Promise`
111 |
112 | Initialize or connect to a key-value store. It uses [localStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) if the corresponding `Tome` has no Urbit connection.
113 |
114 | ```typescript
115 | const db = await Tome.init(api, 'demo', {
116 | realm: true
117 | })
118 |
119 | const kv = await db.keyvalue({
120 | bucket: 'preferences',
121 | permissions: { read: 'space', write: 'space', admin: 'our' },
122 | preload: true,
123 | onDataChange: setData,
124 | onReadyChange: setReady,
125 | onWriteChange: setWrite,
126 | onAdminChange: setAdmin
127 | })
128 | ```
129 |
130 | {% content-ref url="key-value-api.md" %}
131 | [key-value-api.md](key-value-api.md)
132 | {% endcontent-ref %}
133 |
134 | ## `Feed + Log`
135 |
136 | `db.feed(options?)` or `db.log(options?)`
137 |
138 |
139 |
140 | options
141 |
142 | Optional `bucket`, `permissions`, `preload` flag, and callbacks for the log or feed store.
143 |
144 | ```typescript
145 | options: {
146 | bucket?: string = 'def'
147 | preload?: boolean = true
148 | permissions?: Perm
149 | onDataChange?: (data: FeedlogEntry[]) => void
150 | onLoadChange?: (loaded: boolean) => void
151 | onReadyChange?: (ready: boolean) => void
152 | onWriteChange?: (write: boolean) => void
153 | onAdminChange?: (admin: boolean) => void
154 | }
155 | ```
156 |
157 | `permissions` is the permissions for the feedlog store. If not set, defaults to the Tome-level permissions.
158 |
159 | * `read` can read any posts and metadata from the bucket.
160 | * For a `feed`, `write` can create or update their own posts / links. For a `log`, `write` only allows creating new posts.
161 | * `admin` can create or overwrite any posts / links in the bucket.
162 |
163 | Refer to the options under [key-value](initializing-stores.md#key-value) for more information, as the rest are functionally identical.
164 |
165 |
166 |
167 | **Returns**: `Promise` or `Promise`
168 |
169 | Initialize a feed or log store. These must be created from a `Tome` with a valid Urbit connection.
170 |
171 | {% hint style="info" %}
172 | `feed` and `log` are very similar, the only differences are:
173 |
174 | * `write` for a log can't update any values. This makes a log more similar to an "append-only" data structure.
175 | * "Links" (comments, reactions, etc.) currently aren't supported for logs but are for feeds.
176 | {% endhint %}
177 |
178 | ```typescript
179 | const db = await Tome.init(api, 'demo', {
180 | realm: true
181 | })
182 |
183 | const feed = await db.feed({
184 | bucket: 'posts',
185 | preload: true,
186 | permissions: { read: 'space', write: 'space', admin: 'our' },
187 | onLoadChange: setLoaded,
188 | onDataChange: (data) => {
189 | // newest records first.
190 | // if you want a different order, you can sort the data here.
191 | // need to spread array to trigger re-render
192 | setData([...data])
193 | },
194 | onWriteChange: setWrite,
195 | onAdminChange: setAdmin
196 | })
197 | ```
198 |
199 | {% content-ref url="feed-+-log-api.md" %}
200 | [feed-+-log-api.md](feed-+-log-api.md)
201 | {% endcontent-ref %}
202 |
--------------------------------------------------------------------------------
/desk/mar/txt.hoon:
--------------------------------------------------------------------------------
1 | ::
2 | :::: /hoon/txt/mar
3 | ::
4 | /? 310
5 | ::
6 | =, clay
7 | =, differ
8 | =, format
9 | =, mimes:html
10 | |_ txt=wain
11 | ::
12 | ++ grab :: convert from
13 | |%
14 | ++ mime |=((pair mite octs) (to-wain q.q))
15 | ++ noun wain :: clam from %noun
16 | --
17 | ++ grow
18 | => v=.
19 | |%
20 | ++ mime => v [/text/plain (as-octs (of-wain txt))]
21 | ++ elem => v ;pre: {(trip (of-wain txt))}
22 | --
23 | ++ grad
24 | |%
25 | ++ form %txt-diff
26 | ++ diff
27 | |= tyt=wain
28 | ^- (urge cord)
29 | (lusk txt tyt (loss txt tyt))
30 | ::
31 | ++ pact
32 | |= dif=(urge cord)
33 | ~| [%pacting dif]
34 | ^- wain
35 | (lurk txt dif)
36 | ::
37 | ++ join
38 | |= [ali=(urge cord) bob=(urge cord)]
39 | ^- (unit (urge cord))
40 | |^
41 | =. ali (clean ali)
42 | =. bob (clean bob)
43 | |- ^- (unit (urge cord))
44 | ?~ ali `bob
45 | ?~ bob `ali
46 | ?- -.i.ali
47 | %&
48 | ?- -.i.bob
49 | %&
50 | ?: =(p.i.ali p.i.bob)
51 | %+ bind $(ali t.ali, bob t.bob)
52 | |=(cud=(urge cord) [i.ali cud])
53 | ?: (gth p.i.ali p.i.bob)
54 | %+ bind $(p.i.ali (sub p.i.ali p.i.bob), bob t.bob)
55 | |=(cud=(urge cord) [i.bob cud])
56 | %+ bind $(ali t.ali, p.i.bob (sub p.i.bob p.i.ali))
57 | |=(cud=(urge cord) [i.ali cud])
58 | ::
59 | %|
60 | ?: =(p.i.ali (lent p.i.bob))
61 | %+ bind $(ali t.ali, bob t.bob)
62 | |=(cud=(urge cord) [i.bob cud])
63 | ?: (gth p.i.ali (lent p.i.bob))
64 | %+ bind $(p.i.ali (sub p.i.ali (lent p.i.bob)), bob t.bob)
65 | |=(cud=(urge cord) [i.bob cud])
66 | ~
67 | ==
68 | ::
69 | %|
70 | ?- -.i.bob
71 | %|
72 | ?. =(i.ali i.bob)
73 | ~
74 | %+ bind $(ali t.ali, bob t.bob)
75 | |=(cud=(urge cord) [i.ali cud])
76 | ::
77 | %&
78 | ?: =(p.i.bob (lent p.i.ali))
79 | %+ bind $(ali t.ali, bob t.bob)
80 | |=(cud=(urge cord) [i.ali cud])
81 | ?: (gth p.i.bob (lent p.i.ali))
82 | %+ bind $(ali t.ali, p.i.bob (sub p.i.bob (lent p.i.ali)))
83 | |=(cud=(urge cord) [i.ali cud])
84 | ~
85 | ==
86 | ==
87 | ++ clean :: clean
88 | |= wig=(urge cord)
89 | ^- (urge cord)
90 | ?~ wig ~
91 | ?~ t.wig wig
92 | ?: ?=(%& -.i.wig)
93 | ?: ?=(%& -.i.t.wig)
94 | $(wig [[%& (add p.i.wig p.i.t.wig)] t.t.wig])
95 | [i.wig $(wig t.wig)]
96 | ?: ?=(%| -.i.t.wig)
97 | $(wig [[%| (welp p.i.wig p.i.t.wig) (welp q.i.wig q.i.t.wig)] t.t.wig])
98 | [i.wig $(wig t.wig)]
99 | --
100 | ::
101 | ++ mash
102 | |= $: [als=ship ald=desk ali=(urge cord)]
103 | [bos=ship bod=desk bob=(urge cord)]
104 | ==
105 | ^- (urge cord)
106 | |^
107 | =. ali (clean ali)
108 | =. bob (clean bob)
109 | |- ^- (urge cord)
110 | ?~ ali bob
111 | ?~ bob ali
112 | ?- -.i.ali
113 | %&
114 | ?- -.i.bob
115 | %&
116 | ?: =(p.i.ali p.i.bob)
117 | [i.ali $(ali t.ali, bob t.bob)]
118 | ?: (gth p.i.ali p.i.bob)
119 | [i.bob $(p.i.ali (sub p.i.ali p.i.bob), bob t.bob)]
120 | [i.ali $(ali t.ali, p.i.bob (sub p.i.bob p.i.ali))]
121 | ::
122 | %|
123 | ?: =(p.i.ali (lent p.i.bob))
124 | [i.bob $(ali t.ali, bob t.bob)]
125 | ?: (gth p.i.ali (lent p.i.bob))
126 | [i.bob $(p.i.ali (sub p.i.ali (lent p.i.bob)), bob t.bob)]
127 | =/ [fic=(unce cord) ali=(urge cord) bob=(urge cord)]
128 | (resolve ali bob)
129 | [fic $(ali ali, bob bob)]
130 | :: ~ :: here, alice is good for a while, but not for the whole
131 | == :: length of bob's changes
132 | ::
133 | %|
134 | ?- -.i.bob
135 | %|
136 | =/ [fic=(unce cord) ali=(urge cord) bob=(urge cord)]
137 | (resolve ali bob)
138 | [fic $(ali ali, bob bob)]
139 | ::
140 | %&
141 | ?: =(p.i.bob (lent p.i.ali))
142 | [i.ali $(ali t.ali, bob t.bob)]
143 | ?: (gth p.i.bob (lent p.i.ali))
144 | [i.ali $(ali t.ali, p.i.bob (sub p.i.bob (lent p.i.ali)))]
145 | =/ [fic=(unce cord) ali=(urge cord) bob=(urge cord)]
146 | (resolve ali bob)
147 | [fic $(ali ali, bob bob)]
148 | ==
149 | ==
150 | ::
151 | ++ annotate :: annotate conflict
152 | |= $: ali=(list @t)
153 | bob=(list @t)
154 | bas=(list @t)
155 | ==
156 | ^- (list @t)
157 | %- zing
158 | ^- (list (list @t))
159 | %- flop
160 | ^- (list (list @t))
161 | :- :_ ~
162 | %^ cat 3 '<<<<<<<<<<<<'
163 | %^ cat 3 ' '
164 | %^ cat 3 `@t`(scot %p bos)
165 | %^ cat 3 '/'
166 | bod
167 |
168 | :- bob
169 | :- ~['------------']
170 | :- bas
171 | :- ~['++++++++++++']
172 | :- ali
173 | :- :_ ~
174 | %^ cat 3 '>>>>>>>>>>>>'
175 | %^ cat 3 ' '
176 | %^ cat 3 `@t`(scot %p als)
177 | %^ cat 3 '/'
178 | ald
179 | ~
180 | ::
181 | ++ clean :: clean
182 | |= wig=(urge cord)
183 | ^- (urge cord)
184 | ?~ wig ~
185 | ?~ t.wig wig
186 | ?: ?=(%& -.i.wig)
187 | ?: ?=(%& -.i.t.wig)
188 | $(wig [[%& (add p.i.wig p.i.t.wig)] t.t.wig])
189 | [i.wig $(wig t.wig)]
190 | ?: ?=(%| -.i.t.wig)
191 | $(wig [[%| (welp p.i.wig p.i.t.wig) (welp q.i.wig q.i.t.wig)] t.t.wig])
192 | [i.wig $(wig t.wig)]
193 | ::
194 | ++ resolve
195 | |= [ali=(urge cord) bob=(urge cord)]
196 | ^- [fic=[%| p=(list cord) q=(list cord)] ali=(urge cord) bob=(urge cord)]
197 | =- [[%| bac (annotate alc boc bac)] ali bob]
198 | |- ^- $: $: bac=(list cord)
199 | alc=(list cord)
200 | boc=(list cord)
201 | ==
202 | ali=(urge cord)
203 | bob=(urge cord)
204 | ==
205 | ?~ ali [[~ ~ ~] ali bob]
206 | ?~ bob [[~ ~ ~] ali bob]
207 | ?- -.i.ali
208 | %&
209 | ?- -.i.bob
210 | %& [[~ ~ ~] ali bob] :: no conflict
211 | %|
212 | =+ lob=(lent p.i.bob)
213 | ?: =(lob p.i.ali)
214 | [[p.i.bob p.i.bob q.i.bob] t.ali t.bob]
215 | ?: (lth lob p.i.ali)
216 | [[p.i.bob p.i.bob q.i.bob] [[%& (sub p.i.ali lob)] t.ali] t.bob]
217 | =+ wat=(scag (sub lob p.i.ali) p.i.bob)
218 | =+ ^= res
219 | %= $
220 | ali t.ali
221 | bob [[%| (scag (sub lob p.i.ali) p.i.bob) ~] t.bob]
222 | ==
223 | :* :* (welp bac.res wat)
224 | (welp alc.res wat)
225 | (welp boc.res q.i.bob)
226 | ==
227 | ali.res
228 | bob.res
229 | ==
230 | ==
231 | ::
232 | %|
233 | ?- -.i.bob
234 | %&
235 | =+ loa=(lent p.i.ali)
236 | ?: =(loa p.i.bob)
237 | [[p.i.ali q.i.ali p.i.ali] t.ali t.bob]
238 | ?: (lth loa p.i.bob)
239 | [[p.i.ali q.i.ali p.i.ali] t.ali [[%& (sub p.i.bob loa)] t.bob]]
240 | =+ wat=(slag (sub loa p.i.bob) p.i.ali)
241 | =+ ^= res
242 | %= $
243 | ali [[%| (scag (sub loa p.i.bob) p.i.ali) ~] t.ali]
244 | bob t.bob
245 | ==
246 | :* :* (welp bac.res wat)
247 | (welp alc.res q.i.ali)
248 | (welp boc.res wat)
249 | ==
250 | ali.res
251 | bob.res
252 | ==
253 | ::
254 | %|
255 | =+ loa=(lent p.i.ali)
256 | =+ lob=(lent p.i.bob)
257 | ?: =(loa lob)
258 | [[p.i.ali q.i.ali q.i.bob] t.ali t.bob]
259 | =+ ^= res
260 | ?: (gth loa lob)
261 | $(ali [[%| (scag (sub loa lob) p.i.ali) ~] t.ali], bob t.bob)
262 | ~& [%scagging loa=loa pibob=p.i.bob slag=(scag loa p.i.bob)]
263 | $(ali t.ali, bob [[%| (scag (sub lob loa) p.i.bob) ~] t.bob])
264 | :* :* (welp bac.res ?:((gth loa lob) p.i.bob p.i.ali))
265 | (welp alc.res q.i.ali)
266 | (welp boc.res q.i.bob)
267 | ==
268 | ali.res
269 | bob.res
270 | ==
271 | ==
272 | ==
273 | --
274 | --
275 | --
276 |
--------------------------------------------------------------------------------
/desk/lib/spaces.hoon:
--------------------------------------------------------------------------------
1 | /- store=spaces-store, member-store=membership, visas
2 | /+ memb-lib=membership
3 | =< [store .]
4 | =, store
5 | |%
6 |
7 | ++ create-space
8 | |= [=ship slug=@t payload=add-payload:store updated-at=@da]
9 | ^- space:store
10 | =/ default-theme
11 | [
12 | mode=%light
13 | background-color='#C4C3BF'
14 | accent-color='#4E9EFD'
15 | input-color='#fff'
16 | dock-color='#fff'
17 | icon-color='rgba(95,94,88,0.3)'
18 | text-color='#333333'
19 | window-color='#fff'
20 | wallpaper='https://images.unsplash.com/photo-1622547748225-3fc4abd2cca0?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2832&q=100'
21 | ]
22 | =/ new-space
23 | [
24 | path=[ship slug]
25 | name=name:payload
26 | description=description:payload
27 | type=type:payload
28 | access=access:payload
29 | picture=picture:payload
30 | color=color:payload
31 | archetype=archetype:payload
32 | theme=default-theme
33 | updated-at=updated-at
34 | ]
35 | new-space
36 | ::
37 | :: json
38 | ::
39 | ++ enjs
40 | =, enjs:format
41 | |%
42 | ++ reaction
43 | |= rct=^reaction
44 | ^- json
45 | %- pairs
46 | :_ ~
47 | ^- [cord json]
48 | ?- -.rct
49 | %initial
50 | :- %initial
51 | %- pairs
52 | :~ [%spaces (spaces-map:encode spaces.rct)]
53 | [%membership (membership-map:encode membership.rct)]
54 | [%invitations (invitations:encode invitations.rct)]
55 | ==
56 | ::
57 | %add
58 | :- %add
59 | %- pairs
60 | :~ [%space (spc:encode space.rct)]
61 | [%members (membs:encode members.rct)]
62 | ==
63 | ::
64 | %replace
65 | :- %replace
66 | %- pairs
67 | :~ [%space (spc:encode space.rct)]
68 | ==
69 | ::
70 | %remove
71 | :- %remove
72 | %- pairs
73 | :~ [%space-path s+(spat /(scot %p ship.path.rct)/(scot %tas space.path.rct))]
74 | ==
75 | ::
76 | %remote-space
77 | :- %remote-space
78 | %- pairs
79 | :~ [%path s+(spat /(scot %p ship.path.rct)/(scot %tas space.path.rct))]
80 | [%space (spc:encode space.rct)]
81 | :: [%members (passes:encode:membership membership.rct)]
82 | [%members (membs:encode members.rct)]
83 | ==
84 |
85 |
86 | :: %members
87 | :: :- %members
88 | :: %- pairs
89 | :: :~ [%path s+(spat /(scot %p ship.path.rct)/(scot %tas space.path.rct))]
90 | :: [%members (membership-json:encode:memb-lib membership.rct)]
91 | :: ==
92 | ==
93 | ::
94 | ++ view :: encodes for on-peek
95 | |= vi=^view
96 | ^- json
97 | %- pairs
98 | :_ ~
99 | ^- [cord json]
100 | :- -.vi
101 | ?- -.vi
102 | ::
103 | %space
104 | (spc:encode space.vi)
105 | ::
106 | %spaces
107 | (spaces-map:encode spaces.vi)
108 | ==
109 | --
110 |
111 | ::
112 | ++ dejs
113 | =, dejs:format
114 | |%
115 | ++ action
116 | |= jon=json
117 | ^- ^action
118 | =< (decode jon)
119 | |%
120 | ++ decode
121 | %- of
122 | :~ [%add add-space]
123 | [%update update-space]
124 | [%remove path-key]
125 | [%join path-key]
126 | [%leave path-key]
127 | :: [%kicked kicked]
128 | ==
129 | ::
130 | ++ de-space
131 | %- ot
132 | :~ [%path pth]
133 | [%name so]
134 | [%type space-type]
135 | [%access access]
136 | [%picture so]
137 | [%color so]
138 | [%archetype archetype]
139 | [%theme thm]
140 | [%updated-at di]
141 | ==
142 | ::
143 | ++ add-space
144 | %- ot
145 | :~ [%slug so]
146 | [%payload add-payload]
147 | [%members (op ;~(pfix sig fed:ag) memb)]
148 | ==
149 | ::
150 | ++ update-space
151 | %- ot
152 | :~ [%path pth]
153 | [%payload edit-payload]
154 | ==
155 | ::
156 | ++ kicked
157 | %- ot
158 | :~ [%path pth]
159 | [%ship (su ;~(pfix sig fed:ag))]
160 | ==
161 | ::
162 | ++ path-key
163 | %- ot
164 | :~ [%path pth]
165 | ==
166 | ::
167 | ++ pth
168 | %- ot
169 | :~ [%ship (su ;~(pfix sig fed:ag))]
170 | [%space so]
171 | ==
172 | ::
173 | ++ add-payload
174 | %- ot
175 | :~ [%name so]
176 | [%description so]
177 | [%type space-type]
178 | [%access access]
179 | [%picture so]
180 | [%color so]
181 | [%archetype archetype]
182 | ==
183 | ::
184 | ++ edit-payload
185 | %- ot
186 | :~ [%name so]
187 | [%description so]
188 | [%access (su (perk %public %private ~))]
189 | [%picture so]
190 | [%color so]
191 | [%theme thm]
192 | ==
193 | ::
194 | ++ thm
195 | %- ot
196 | :~ [%mode theme-mode]
197 | [%background-color so]
198 | [%accent-color so]
199 | [%input-color so]
200 | [%dock-color so]
201 | [%icon-color so]
202 | [%text-color so]
203 | [%window-color so]
204 | [%wallpaper so]
205 | ==
206 | ::
207 | ++ memb
208 | %- ot
209 | :~ [%roles (as rol)]
210 | [%alias so]
211 | [%status status]
212 | :: [%pinned bo]
213 | ==
214 | ::
215 | ++ theme-mode
216 | |= =json
217 | ^- theme-mode:store
218 | ?> ?=(%s -.json)
219 | ?: =('light' p.json) %light
220 | ?: =('dark' p.json) %dark
221 | !!
222 | ::
223 | ++ space-type
224 | |= =json
225 | ^- space-type:store
226 | ?> ?=(%s -.json)
227 | ?: =('group' p.json) %group
228 | ?: =('our' p.json) %our
229 | ?: =('space' p.json) %space
230 | !!
231 | ::
232 | ++ rol
233 | |= =json
234 | ^- role:member-store
235 | ?> ?=(%s -.json)
236 | ?: =('initiate' p.json) %initiate
237 | ?: =('member' p.json) %member
238 | ?: =('admin' p.json) %admin
239 | ?: =('owner' p.json) %owner
240 | !!
241 | ::
242 | ++ archetype
243 | |= =json
244 | ^- archetype:store
245 | ?> ?=(%s -.json)
246 | ?: =('home' p.json) %home
247 | ?: =('community' p.json) %community
248 | ?: =('creator-dao' p.json) %creator-dao
249 | ?: =('service-dao' p.json) %service-dao
250 | ?: =('investment-dao' p.json) %investment-dao
251 | !!
252 | ::
253 | ++ access
254 | |= =json
255 | ^- space-access:store
256 | ?> ?=(%s -.json)
257 | ?: =('public' p.json) %public
258 | ?: =('antechamber' p.json) %antechamber
259 | ?: =('private' p.json) %private
260 | !!
261 | ::
262 | ++ status
263 | |= =json
264 | ^- status:member-store
265 | ?> ?=(%s -.json)
266 | ?: =('invited' p.json) %invited
267 | ?: =('joined' p.json) %joined
268 | ?: =('host' p.json) %host
269 | !!
270 | --
271 | --
272 | ::
273 | ::
274 | ::
275 | ++ encode
276 | =, enjs:format
277 | |%
278 | ++ spaces-map
279 | |= =spaces:store
280 | ^- json
281 | %- pairs
282 | %+ turn ~(tap by spaces)
283 | |= [pth=space-path:store space=space:store]
284 | =/ spc-path (spat /(scot %p ship.pth)/(scot %tas space.pth))
285 | ^- [cord json]
286 | [spc-path (spc space)]
287 | ::
288 | ++ membership-map
289 | |= =membership:member-store
290 | ^- json
291 | %- pairs
292 | %+ turn ~(tap by membership)
293 | |= [pth=space-path:store members=members:member-store]
294 | =/ spc-path (spat /(scot %p ship.pth)/(scot %tas space.pth))
295 | ^- [cord json]
296 | [spc-path (membs members)]
297 | ::
298 | ++ membs
299 | |= =members:member-store
300 | ^- json
301 | %- pairs
302 | %+ turn ~(tap by members)
303 | |= [=^ship =member:member-store]
304 | ^- [cord json]
305 | [(scot %p ship) (memb member)]
306 | ::
307 | ++ memb
308 | |= =member:member-store
309 | ^- json
310 | %- pairs
311 | :~ ['roles' a+(turn ~(tap in roles.member) |=(rol=role:member-store s+(scot %tas rol)))]
312 | ['status' s+(scot %tas status.member)]
313 | :: ['pinned' b+pinned.member]
314 | ==
315 | ::
316 | ++ spc
317 | |= =space
318 | ^- json
319 | %- pairs
320 | :~ ['path' s+(spat /(scot %p ship.path.space)/(scot %tas space.path.space))]
321 | ['name' s+name.space]
322 | ['description' s+description.space]
323 | ['access' s+access.space]
324 | ['type' s+type.space]
325 | ['picture' s+picture.space]
326 | ['color' s+color.space]
327 | ['theme' (thm theme.space)]
328 | ['updatedAt' (time updated-at.space)]
329 | ==
330 | ::
331 | ++ thm
332 | |= =theme
333 | ^- json
334 | %- pairs
335 | :~
336 | ['mode' s+(scot %tas mode.theme)]
337 | ['backgroundColor' s+background-color.theme]
338 | ['accentColor' s+accent-color.theme]
339 | ['inputColor' s+input-color.theme]
340 | ['dockColor' s+dock-color.theme]
341 | ['iconColor' s+icon-color.theme]
342 | ['textColor' s+text-color.theme]
343 | ['windowColor' s+window-color.theme]
344 | ['wallpaper' s+wallpaper.theme]
345 | ==
346 | ::
347 | ++ invitations
348 | |= =invitations:visas
349 | ^- json
350 | %- pairs
351 | %+ turn ~(tap by invitations)
352 | |= [pth=space-path:store inv=invite:visas]
353 | =/ spc-path (spat /(scot %p ship.pth)/(scot %tas space.pth))
354 | ^- [cord json]
355 | [spc-path (invite inv)]
356 | ::
357 | ++ invite
358 | |= =invite:visas
359 | ^- json
360 | %- pairs:enjs:format
361 | :~ ['inviter' s+(scot %p inviter.invite)]
362 | ['path' s+(spat /(scot %p ship.path.invite)/(scot %tas space.path.invite))]
363 | ['role' s+(scot %tas role.invite)]
364 | ['message' s+message.invite]
365 | ['name' s+name.invite]
366 | ['type' s+type.invite]
367 | ['picture' s+picture.invite]
368 | ['color' s+color.invite]
369 | ['invitedAt' (time invited-at.invite)]
370 | ==
371 | ::
372 | --
373 | --
374 |
--------------------------------------------------------------------------------
/desk/lib/strandio.hoon:
--------------------------------------------------------------------------------
1 | /- spider
2 | /+ libstrand=strand
3 | =, strand=strand:libstrand
4 | =, strand-fail=strand-fail:libstrand
5 | |%
6 | ++ send-raw-cards
7 | |= cards=(list =card:agent:gall)
8 | =/ m (strand ,~)
9 | ^- form:m
10 | |= strand-input:strand
11 | [cards %done ~]
12 | ::
13 | ++ send-raw-card
14 | |= =card:agent:gall
15 | =/ m (strand ,~)
16 | ^- form:m
17 | (send-raw-cards card ~)
18 | ::
19 | ++ ignore
20 | |= tin=strand-input:strand
21 | `[%fail %ignore ~]
22 | ::
23 | ++ get-bowl
24 | =/ m (strand ,bowl:strand)
25 | ^- form:m
26 | |= tin=strand-input:strand
27 | `[%done bowl.tin]
28 | ::
29 | ++ get-beak
30 | =/ m (strand ,beak)
31 | ^- form:m
32 | |= tin=strand-input:strand
33 | `[%done [our q.byk da+now]:bowl.tin]
34 | ::
35 | ++ get-time
36 | =/ m (strand ,@da)
37 | ^- form:m
38 | |= tin=strand-input:strand
39 | `[%done now.bowl.tin]
40 | ::
41 | ++ get-our
42 | =/ m (strand ,ship)
43 | ^- form:m
44 | |= tin=strand-input:strand
45 | `[%done our.bowl.tin]
46 | ::
47 | ++ get-entropy
48 | =/ m (strand ,@uvJ)
49 | ^- form:m
50 | |= tin=strand-input:strand
51 | `[%done eny.bowl.tin]
52 | ::
53 | :: Convert skips to %ignore failures.
54 | ::
55 | :: This tells the main loop to try the next handler.
56 | ::
57 | ++ handle
58 | |* a=mold
59 | =/ m (strand ,a)
60 | |= =form:m
61 | ^- form:m
62 | |= tin=strand-input:strand
63 | =/ res (form tin)
64 | =? next.res ?=(%skip -.next.res)
65 | [%fail %ignore ~]
66 | res
67 | ::
68 | :: Wait for a poke with a particular mark
69 | ::
70 | ++ take-poke
71 | |= =mark
72 | =/ m (strand ,vase)
73 | ^- form:m
74 | |= tin=strand-input:strand
75 | ?+ in.tin `[%skip ~]
76 | ~
77 | `[%wait ~]
78 | ::
79 | [~ %poke @ *]
80 | ?. =(mark p.cage.u.in.tin)
81 | `[%skip ~]
82 | `[%done q.cage.u.in.tin]
83 | ==
84 | ::
85 | ++ take-sign-arvo
86 | =/ m (strand ,[wire sign-arvo])
87 | ^- form:m
88 | |= tin=strand-input:strand
89 | ?+ in.tin `[%skip ~]
90 | ~
91 | `[%wait ~]
92 | ::
93 | [~ %sign *]
94 | `[%done [wire sign-arvo]:u.in.tin]
95 | ==
96 | ::
97 | :: Wait for a subscription update on a wire
98 | ::
99 | ++ take-fact-prefix
100 | |= =wire
101 | =/ m (strand ,[path cage])
102 | ^- form:m
103 | |= tin=strand-input:strand
104 | ?+ in.tin `[%skip ~]
105 | ~ `[%wait ~]
106 | [~ %agent * %fact *]
107 | ?. =(watch+wire (scag +((lent wire)) wire.u.in.tin))
108 | `[%skip ~]
109 | `[%done (slag (lent wire) wire.u.in.tin) cage.sign.u.in.tin]
110 | ==
111 | ::
112 | :: Wait for a subscription update on a wire
113 | ::
114 | ++ take-fact
115 | |= =wire
116 | =/ m (strand ,cage)
117 | ^- form:m
118 | |= tin=strand-input:strand
119 | ?+ in.tin `[%skip ~]
120 | ~ `[%wait ~]
121 | [~ %agent * %fact *]
122 | ?. =(watch+wire wire.u.in.tin)
123 | `[%skip ~]
124 | `[%done cage.sign.u.in.tin]
125 | ==
126 | ::
127 | :: Wait for a subscription close
128 | ::
129 | ++ take-kick
130 | |= =wire
131 | =/ m (strand ,~)
132 | ^- form:m
133 | |= tin=strand-input:strand
134 | ?+ in.tin `[%skip ~]
135 | ~ `[%wait ~]
136 | [~ %agent * %kick *]
137 | ?. =(watch+wire wire.u.in.tin)
138 | `[%skip ~]
139 | `[%done ~]
140 | ==
141 | ::
142 | ++ echo
143 | =/ m (strand ,~)
144 | ^- form:m
145 | %- (main-loop ,~)
146 | :~ |= ~
147 | ^- form:m
148 | ;< =vase bind:m ((handle ,vase) (take-poke %echo))
149 | =/ message=tape !<(tape vase)
150 | %- (slog leaf+"{message}..." ~)
151 | ;< ~ bind:m (sleep ~s2)
152 | %- (slog leaf+"{message}.." ~)
153 | (pure:m ~)
154 | ::
155 | |= ~
156 | ^- form:m
157 | ;< =vase bind:m ((handle ,vase) (take-poke %over))
158 | %- (slog leaf+"over..." ~)
159 | (pure:m ~)
160 | ==
161 | ::
162 | ++ take-watch
163 | =/ m (strand ,path)
164 | |= tin=strand-input:strand
165 | ?+ in.tin `[%skip ~]
166 | ~ `[%wait ~]
167 | [~ %watch *]
168 | `[%done path.u.in.tin]
169 | ==
170 | ::
171 | ++ take-wake
172 | |= until=(unit @da)
173 | =/ m (strand ,~)
174 | ^- form:m
175 | |= tin=strand-input:strand
176 | ?+ in.tin `[%skip ~]
177 | ~ `[%wait ~]
178 | [~ %sign [%wait @ ~] %behn %wake *]
179 | ?. |(?=(~ until) =(`u.until (slaw %da i.t.wire.u.in.tin)))
180 | `[%skip ~]
181 | ?~ error.sign-arvo.u.in.tin
182 | `[%done ~]
183 | `[%fail %timer-error u.error.sign-arvo.u.in.tin]
184 | ==
185 | ::
186 | ++ take-tune
187 | |= =wire
188 | =/ m (strand ,[spar:ames (unit roar:ames)])
189 | ^- form:m
190 | |= tin=strand-input:strand
191 | ?+ in.tin `[%skip ~]
192 | ~ `[%wait ~]
193 | ::
194 | [~ %sign * %ames %tune ^ *]
195 | ?. =(wire wire.u.in.tin)
196 | `[%skip ~]
197 | `[%done +>.sign-arvo.u.in.tin]
198 | ==
199 | ::
200 | ++ take-poke-ack
201 | |= =wire
202 | =/ m (strand ,~)
203 | ^- form:m
204 | |= tin=strand-input:strand
205 | ?+ in.tin `[%skip ~]
206 | ~ `[%wait ~]
207 | [~ %agent * %poke-ack *]
208 | ?. =(wire wire.u.in.tin)
209 | `[%skip ~]
210 | ?~ p.sign.u.in.tin
211 | `[%done ~]
212 | `[%fail %poke-fail u.p.sign.u.in.tin]
213 | ==
214 | ::
215 | ++ take-watch-ack
216 | |= =wire
217 | =/ m (strand ,~)
218 | ^- form:m
219 | |= tin=strand-input:strand
220 | ?+ in.tin `[%skip ~]
221 | ~ `[%wait ~]
222 | [~ %agent * %watch-ack *]
223 | ?. =(watch+wire wire.u.in.tin)
224 | `[%skip ~]
225 | ?~ p.sign.u.in.tin
226 | `[%done ~]
227 | `[%fail %watch-ack-fail u.p.sign.u.in.tin]
228 | ==
229 | ::
230 | ++ poke
231 | |= [=dock =cage]
232 | =/ m (strand ,~)
233 | ^- form:m
234 | =/ =card:agent:gall [%pass /poke %agent dock %poke cage]
235 | ;< ~ bind:m (send-raw-card card)
236 | (take-poke-ack /poke)
237 | ::
238 | ++ raw-poke
239 | |= [=dock =cage]
240 | =/ m (strand ,~)
241 | ^- form:m
242 | =/ =card:agent:gall [%pass /poke %agent dock %poke cage]
243 | ;< ~ bind:m (send-raw-card card)
244 | =/ m (strand ,~)
245 | ^- form:m
246 | |= tin=strand-input:strand
247 | ?+ in.tin `[%skip ~]
248 | ~
249 | `[%wait ~]
250 | ::
251 | [~ %agent * %poke-ack *]
252 | ?. =(/poke wire.u.in.tin)
253 | `[%skip ~]
254 | `[%done ~]
255 | ==
256 | ::
257 | ++ raw-poke-our
258 | |= [app=term =cage]
259 | =/ m (strand ,~)
260 | ^- form:m
261 | ;< =bowl:spider bind:m get-bowl
262 | (raw-poke [our.bowl app] cage)
263 | ::
264 | ++ poke-our
265 | |= [=term =cage]
266 | =/ m (strand ,~)
267 | ^- form:m
268 | ;< our=@p bind:m get-our
269 | (poke [our term] cage)
270 | ::
271 | ++ watch
272 | |= [=wire =dock =path]
273 | =/ m (strand ,~)
274 | ^- form:m
275 | =/ =card:agent:gall [%pass watch+wire %agent dock %watch path]
276 | ;< ~ bind:m (send-raw-card card)
277 | (take-watch-ack wire)
278 | ::
279 | ++ watch-one
280 | |= [=wire =dock =path]
281 | =/ m (strand ,cage)
282 | ^- form:m
283 | ;< ~ bind:m (watch wire dock path)
284 | ;< =cage bind:m (take-fact wire)
285 | ;< ~ bind:m (take-kick wire)
286 | (pure:m cage)
287 | ::
288 | ++ watch-our
289 | |= [=wire =term =path]
290 | =/ m (strand ,~)
291 | ^- form:m
292 | ;< our=@p bind:m get-our
293 | (watch wire [our term] path)
294 | ::
295 | ++ scry
296 | |* [=mold =path]
297 | =/ m (strand ,mold)
298 | ^- form:m
299 | ?> ?=(^ path)
300 | ?> ?=(^ t.path)
301 | ;< =bowl:spider bind:m get-bowl
302 | %- pure:m
303 | .^(mold i.path (scot %p our.bowl) i.t.path (scot %da now.bowl) t.t.path)
304 | ::
305 | ++ leave
306 | |= [=wire =dock]
307 | =/ m (strand ,~)
308 | ^- form:m
309 | =/ =card:agent:gall [%pass watch+wire %agent dock %leave ~]
310 | (send-raw-card card)
311 | ::
312 | ++ leave-our
313 | |= [=wire =term]
314 | =/ m (strand ,~)
315 | ^- form:m
316 | ;< our=@p bind:m get-our
317 | (leave wire [our term])
318 | ::
319 | ++ rewatch
320 | |= [=wire =dock =path]
321 | =/ m (strand ,~)
322 | ;< ~ bind:m ((handle ,~) (take-kick wire))
323 | ;< ~ bind:m (flog-text "rewatching {} {}")
324 | ;< ~ bind:m (watch wire dock path)
325 | (pure:m ~)
326 | ::
327 | ++ wait
328 | |= until=@da
329 | =/ m (strand ,~)
330 | ^- form:m
331 | ;< ~ bind:m (send-wait until)
332 | (take-wake `until)
333 | ::
334 | ++ keen
335 | |= [=wire =spar:ames]
336 | =/ m (strand ,~)
337 | ^- form:m
338 | (send-raw-card %pass wire %arvo %a %keen spar)
339 | ::
340 | ++ sleep
341 | |= for=@dr
342 | =/ m (strand ,~)
343 | ^- form:m
344 | ;< now=@da bind:m get-time
345 | (wait (add now for))
346 | ::
347 | ++ send-wait
348 | |= until=@da
349 | =/ m (strand ,~)
350 | ^- form:m
351 | =/ =card:agent:gall
352 | [%pass /wait/(scot %da until) %arvo %b %wait until]
353 | (send-raw-card card)
354 | ::
355 | ++ map-err
356 | |* computation-result=mold
357 | =/ m (strand ,computation-result)
358 | |= [f=$-([term tang] [term tang]) computation=form:m]
359 | ^- form:m
360 | |= tin=strand-input:strand
361 | =* loop $
362 | =/ c-res (computation tin)
363 | ?: ?=(%cont -.next.c-res)
364 | c-res(self.next ..loop(computation self.next.c-res))
365 | ?. ?=(%fail -.next.c-res)
366 | c-res
367 | c-res(err.next (f err.next.c-res))
368 | ::
369 | ++ set-timeout
370 | |* computation-result=mold
371 | =/ m (strand ,computation-result)
372 | |= [time=@dr computation=form:m]
373 | ^- form:m
374 | ;< now=@da bind:m get-time
375 | =/ when (add now time)
376 | =/ =card:agent:gall
377 | [%pass /timeout/(scot %da when) %arvo %b %wait when]
378 | ;< ~ bind:m (send-raw-card card)
379 | |= tin=strand-input:strand
380 | =* loop $
381 | ?: ?& ?=([~ %sign [%timeout @ ~] %behn %wake *] in.tin)
382 | =((scot %da when) i.t.wire.u.in.tin)
383 | ==
384 | `[%fail %timeout ~]
385 | =/ c-res (computation tin)
386 | ?: ?=(%cont -.next.c-res)
387 | c-res(self.next ..loop(computation self.next.c-res))
388 | ?: ?=(%done -.next.c-res)
389 | =/ =card:agent:gall
390 | [%pass /timeout/(scot %da when) %arvo %b %rest when]
391 | c-res(cards [card cards.c-res])
392 | c-res
393 | ::
394 | ++ send-request
395 | |= =request:http
396 | =/ m (strand ,~)
397 | ^- form:m
398 | (send-raw-card %pass /request %arvo %i %request request *outbound-config:iris)
399 | ::
400 | ++ send-cancel-request
401 | =/ m (strand ,~)
402 | ^- form:m
403 | (send-raw-card %pass /request %arvo %i %cancel-request ~)
404 | ::
405 | ++ take-client-response
406 | =/ m (strand ,client-response:iris)
407 | ^- form:m
408 | |= tin=strand-input:strand
409 | ?+ in.tin `[%skip ~]
410 | ~ `[%wait ~]
411 | ::
412 | [~ %sign [%request ~] %iris %http-response %cancel *]
413 | ::NOTE iris does not (yet?) retry after cancel, so it means failure
414 | :- ~
415 | :+ %fail
416 | %http-request-cancelled
417 | ['http request was cancelled by the runtime']~
418 | ::
419 | [~ %sign [%request ~] %iris %http-response %finished *]
420 | `[%done client-response.sign-arvo.u.in.tin]
421 | ==
422 | ::
423 | :: Wait until we get an HTTP response or cancelation and unset contract
424 | ::
425 | ++ take-maybe-sigh
426 | =/ m (strand ,(unit httr:eyre))
427 | ^- form:m
428 | ;< rep=(unit client-response:iris) bind:m
429 | take-maybe-response
430 | ?~ rep
431 | (pure:m ~)
432 | :: XX s/b impossible
433 | ::
434 | ?. ?=(%finished -.u.rep)
435 | (pure:m ~)
436 | (pure:m (some (to-httr:iris +.u.rep)))
437 | ::
438 | ++ take-maybe-response
439 | =/ m (strand ,(unit client-response:iris))
440 | ^- form:m
441 | |= tin=strand-input:strand
442 | ?+ in.tin `[%skip ~]
443 | ~ `[%wait ~]
444 | [~ %sign [%request ~] %iris %http-response %cancel *]
445 | `[%done ~]
446 | [~ %sign [%request ~] %iris %http-response %finished *]
447 | `[%done `client-response.sign-arvo.u.in.tin]
448 | ==
449 | ::
450 | ++ extract-body
451 | |= =client-response:iris
452 | =/ m (strand ,cord)
453 | ^- form:m
454 | ?> ?=(%finished -.client-response)
455 | %- pure:m
456 | ?~ full-file.client-response ''
457 | q.data.u.full-file.client-response
458 | ::
459 | ++ fetch-cord
460 | |= url=tape
461 | =/ m (strand ,cord)
462 | ^- form:m
463 | =/ =request:http [%'GET' (crip url) ~ ~]
464 | ;< ~ bind:m (send-request request)
465 | ;< =client-response:iris bind:m take-client-response
466 | (extract-body client-response)
467 | ::
468 | ++ fetch-json
469 | |= url=tape
470 | =/ m (strand ,json)
471 | ^- form:m
472 | ;< =cord bind:m (fetch-cord url)
473 | =/ json=(unit json) (de-json:html cord)
474 | ?~ json
475 | (strand-fail %json-parse-error ~)
476 | (pure:m u.json)
477 | ::
478 | ++ hiss-request
479 | |= =hiss:eyre
480 | =/ m (strand ,(unit httr:eyre))
481 | ^- form:m
482 | ;< ~ bind:m (send-request (hiss-to-request:html hiss))
483 | take-maybe-sigh
484 | ::
485 | :: +build-file: build the source file at the specified $beam
486 | ::
487 | ++ build-file
488 | |= [[=ship =desk =case] =spur]
489 | =* arg +<
490 | =/ m (strand ,(unit vase))
491 | ^- form:m
492 | ;< =riot:clay bind:m
493 | (warp ship desk ~ %sing %a case spur)
494 | ?~ riot
495 | (pure:m ~)
496 | ?> =(%vase p.r.u.riot)
497 | (pure:m (some !<(vase q.r.u.riot)))
498 | ::
499 | ++ build-file-hard
500 | |= [[=ship =desk =case] =spur]
501 | =* arg +<
502 | =/ m (strand ,vase)
503 | ^- form:m
504 | ;< =riot:clay
505 | bind:m
506 | (warp ship desk ~ %sing %a case spur)
507 | ?> ?=(^ riot)
508 | ?> ?=(%vase p.r.u.riot)
509 | (pure:m !<(vase q.r.u.riot))
510 | :: +build-mark: build a mark definition to a $dais
511 | ::
512 | ++ build-mark
513 | |= [[=ship =desk =case] mak=mark]
514 | =* arg +<
515 | =/ m (strand ,dais:clay)
516 | ^- form:m
517 | ;< =riot:clay bind:m
518 | (warp ship desk ~ %sing %b case /[mak])
519 | ?~ riot
520 | (strand-fail %build-mark >arg< ~)
521 | ?> =(%dais p.r.u.riot)
522 | (pure:m !<(dais:clay q.r.u.riot))
523 | :: +build-tube: build a mark conversion gate ($tube)
524 | ::
525 | ++ build-tube
526 | |= [[=ship =desk =case] =mars:clay]
527 | =* arg +<
528 | =/ m (strand ,tube:clay)
529 | ^- form:m
530 | ;< =riot:clay bind:m
531 | (warp ship desk ~ %sing %c case /[a.mars]/[b.mars])
532 | ?~ riot
533 | (strand-fail %build-tube >arg< ~)
534 | ?> =(%tube p.r.u.riot)
535 | (pure:m !<(tube:clay q.r.u.riot))
536 | ::
537 | :: +build-nave: build a mark definition to a $nave
538 | ::
539 | ++ build-nave
540 | |= [[=ship =desk =case] mak=mark]
541 | =* arg +<
542 | =/ m (strand ,vase)
543 | ^- form:m
544 | ;< =riot:clay bind:m
545 | (warp ship desk ~ %sing %e case /[mak])
546 | ?~ riot
547 | (strand-fail %build-nave >arg< ~)
548 | ?> =(%nave p.r.u.riot)
549 | (pure:m q.r.u.riot)
550 | :: +build-cast: build a mark conversion gate (static)
551 | ::
552 | ++ build-cast
553 | |= [[=ship =desk =case] =mars:clay]
554 | =* arg +<
555 | =/ m (strand ,vase)
556 | ^- form:m
557 | ;< =riot:clay bind:m
558 | (warp ship desk ~ %sing %f case /[a.mars]/[b.mars])
559 | ?~ riot
560 | (strand-fail %build-cast >arg< ~)
561 | ?> =(%cast p.r.u.riot)
562 | (pure:m q.r.u.riot)
563 | ::
564 | :: Read from Clay
565 | ::
566 | ++ warp
567 | |= [=ship =riff:clay]
568 | =/ m (strand ,riot:clay)
569 | ;< ~ bind:m (send-raw-card %pass /warp %arvo %c %warp ship riff)
570 | (take-writ /warp)
571 | ::
572 | ++ read-file
573 | |= [[=ship =desk =case] =spur]
574 | =* arg +<
575 | =/ m (strand ,cage)
576 | ;< =riot:clay bind:m (warp ship desk ~ %sing %x case spur)
577 | ?~ riot
578 | (strand-fail %read-file >arg< ~)
579 | (pure:m r.u.riot)
580 | ::
581 | ++ check-for-file
582 | |= [[=ship =desk =case] =spur]
583 | =/ m (strand ,?)
584 | ;< =riot:clay bind:m (warp ship desk ~ %sing %x case spur)
585 | (pure:m ?=(^ riot))
586 | ::
587 | ++ list-tree
588 | |= [[=ship =desk =case] =spur]
589 | =* arg +<
590 | =/ m (strand ,(list path))
591 | ;< =riot:clay bind:m (warp ship desk ~ %sing %t case spur)
592 | ?~ riot
593 | (strand-fail %list-tree >arg< ~)
594 | (pure:m !<((list path) q.r.u.riot))
595 | ::
596 | :: Take Clay read result
597 | ::
598 | ++ take-writ
599 | |= =wire
600 | =/ m (strand ,riot:clay)
601 | ^- form:m
602 | |= tin=strand-input:strand
603 | ?+ in.tin `[%skip ~]
604 | ~ `[%wait ~]
605 | [~ %sign * ?(%behn %clay) %writ *]
606 | ?. =(wire wire.u.in.tin)
607 | `[%skip ~]
608 | `[%done +>.sign-arvo.u.in.tin]
609 | ==
610 | :: +check-online: require that peer respond before timeout
611 | ::
612 | ++ check-online
613 | |= [who=ship lag=@dr]
614 | =/ m (strand ,~)
615 | ^- form:m
616 | %+ (map-err ,~) |=(* [%offline *tang])
617 | %+ (set-timeout ,~) lag
618 | ;< ~ bind:m
619 | (poke [who %hood] %helm-hi !>(~))
620 | (pure:m ~)
621 | ::
622 | ++ eval-hoon
623 | |= [gen=hoon bez=(list beam)]
624 | =/ m (strand ,vase)
625 | ^- form:m
626 | =/ sut=vase !>(..zuse)
627 | |-
628 | ?~ bez
629 | (pure:m (slap sut gen))
630 | ;< vax=vase bind:m (build-file-hard i.bez)
631 | $(bez t.bez, sut (slop vax sut))
632 | ::
633 | ++ send-thread
634 | |= [=bear:khan =shed:khan =wire]
635 | =/ m (strand ,~)
636 | ^- form:m
637 | (send-raw-card %pass wire %arvo %k %lard bear shed)
638 | ::
639 | :: Queue on skip, try next on fail %ignore
640 | ::
641 | ++ main-loop
642 | |* a=mold
643 | =/ m (strand ,~)
644 | =/ m-a (strand ,a)
645 | =| queue=(qeu (unit input:strand))
646 | =| active=(unit [in=(unit input:strand) =form:m-a forms=(list $-(a form:m-a))])
647 | =| state=a
648 | |= forms=(lest $-(a form:m-a))
649 | ^- form:m
650 | |= tin=strand-input:strand
651 | =* top `form:m`..$
652 | =. queue (~(put to queue) in.tin)
653 | |^ (continue bowl.tin)
654 | ::
655 | ++ continue
656 | |= =bowl:strand
657 | ^- output:m
658 | ?> =(~ active)
659 | ?: =(~ queue)
660 | `[%cont top]
661 | =^ in=(unit input:strand) queue ~(get to queue)
662 | ^- output:m
663 | =. active `[in (i.forms state) t.forms]
664 | ^- output:m
665 | (run bowl in)
666 | ::
667 | ++ run
668 | ^- form:m
669 | |= tin=strand-input:strand
670 | ^- output:m
671 | ?> ?=(^ active)
672 | =/ res (form.u.active tin)
673 | =/ =output:m
674 | ?- -.next.res
675 | %wait `[%wait ~]
676 | %skip `[%cont ..$(queue (~(put to queue) in.tin))]
677 | %cont `[%cont ..$(active `[in.u.active self.next.res forms.u.active])]
678 | %done (continue(active ~, state value.next.res) bowl.tin)
679 | %fail
680 | ?: &(?=(^ forms.u.active) ?=(%ignore p.err.next.res))
681 | %= $
682 | active `[in.u.active (i.forms.u.active state) t.forms.u.active]
683 | in.tin in.u.active
684 | ==
685 | `[%fail err.next.res]
686 | ==
687 | [(weld cards.res cards.output) next.output]
688 | --
689 | ::
690 | ++ retry
691 | |* result=mold
692 | |= [crash-after=(unit @ud) computation=_*form:(strand (unit result))]
693 | =/ m (strand ,result)
694 | =| try=@ud
695 | |- ^- form:m
696 | =* loop $
697 | ?: =(crash-after `try)
698 | (strand-fail %retry-too-many ~)
699 | ;< ~ bind:m (backoff try ~m1)
700 | ;< res=(unit result) bind:m computation
701 | ?^ res
702 | (pure:m u.res)
703 | loop(try +(try))
704 | ::
705 | ++ backoff
706 | |= [try=@ud limit=@dr]
707 | =/ m (strand ,~)
708 | ^- form:m
709 | ;< eny=@uvJ bind:m get-entropy
710 | %- sleep
711 | %+ min limit
712 | ?: =(0 try) ~s0
713 | %+ add
714 | (mul ~s1 (bex (dec try)))
715 | (mul ~s0..0001 (~(rad og eny) 1.000))
716 | ::
717 | :: ----
718 | ::
719 | :: Output
720 | ::
721 | ++ flog
722 | |= =flog:dill
723 | =/ m (strand ,~)
724 | ^- form:m
725 | (send-raw-card %pass / %arvo %d %flog flog)
726 | ::
727 | ++ flog-text
728 | |= =tape
729 | =/ m (strand ,~)
730 | ^- form:m
731 | (flog %text tape)
732 | ::
733 | ++ flog-tang
734 | |= =tang
735 | =/ m (strand ,~)
736 | ^- form:m
737 | =/ =wall
738 | (zing (turn (flop tang) (cury wash [0 80])))
739 | |- ^- form:m
740 | =* loop $
741 | ?~ wall
742 | (pure:m ~)
743 | ;< ~ bind:m (flog-text i.wall)
744 | loop(wall t.wall)
745 | ::
746 | ++ trace
747 | |= =tang
748 | =/ m (strand ,~)
749 | ^- form:m
750 | (pure:m ((slog tang) ~))
751 | ::
752 | ++ app-message
753 | |= [app=term =cord =tang]
754 | =/ m (strand ,~)
755 | ^- form:m
756 | =/ msg=tape :(weld (trip app) ": " (trip cord))
757 | ;< ~ bind:m (flog-text msg)
758 | (flog-tang tang)
759 | ::
760 | :: ----
761 | ::
762 | :: Handle domains
763 | ::
764 | ++ install-domain
765 | |= =turf
766 | =/ m (strand ,~)
767 | ^- form:m
768 | (send-raw-card %pass / %arvo %e %rule %turf %put turf)
769 | ::
770 | :: ----
771 | ::
772 | :: Threads
773 | ::
774 | ++ start-thread
775 | |= file=term
776 | =/ m (strand ,tid:spider)
777 | ;< =bowl:spider bind:m get-bowl
778 | (start-thread-with-args byk.bowl file *vase)
779 | ::
780 | ++ start-thread-with-args
781 | |= [=beak file=term args=vase]
782 | =/ m (strand ,tid:spider)
783 | ^- form:m
784 | ;< =bowl:spider bind:m get-bowl
785 | =/ tid
786 | (scot %ta (cat 3 (cat 3 'strand_' file) (scot %uv (sham file eny.bowl))))
787 | =/ poke-vase !>(`start-args:spider`[`tid.bowl `tid beak file args])
788 | ;< ~ bind:m (poke-our %spider %spider-start poke-vase)
789 | ;< ~ bind:m (sleep ~s0) :: wait for thread to start
790 | (pure:m tid)
791 | ::
792 | +$ thread-result
793 | (each vase [term tang])
794 | ::
795 | ++ await-thread
796 | |= [file=term args=vase]
797 | =/ m (strand ,thread-result)
798 | ^- form:m
799 | ;< =bowl:spider bind:m get-bowl
800 | =/ tid (scot %ta (cat 3 'strand_' (scot %uv (sham file eny.bowl))))
801 | =/ poke-vase !>(`start-args:spider`[`tid.bowl `tid byk.bowl file args])
802 | ;< ~ bind:m (watch-our /awaiting/[tid] %spider /thread-result/[tid])
803 | ;< ~ bind:m (poke-our %spider %spider-start poke-vase)
804 | ;< ~ bind:m (sleep ~s0) :: wait for thread to start
805 | ;< =cage bind:m (take-fact /awaiting/[tid])
806 | ;< ~ bind:m (take-kick /awaiting/[tid])
807 | ?+ p.cage ~|([%strange-thread-result p.cage file tid] !!)
808 | %thread-done (pure:m %& q.cage)
809 | %thread-fail (pure:m %| ;;([term tang] q.q.cage))
810 | ==
811 | --
812 |
--------------------------------------------------------------------------------
/desk/app/tome.hoon:
--------------------------------------------------------------------------------
1 | :: Tome DB - a Holium collaboration
2 | :: by ~larryx-woldyr
3 | ::
4 | /- *tome, s-p=spaces-path
5 | /+ r-l=realm-lib
6 | /+ verb, dbug, defa=default-agent
7 | /+ *mip
8 | ::
9 | |%
10 | ::
11 | +$ versioned-state $%(state-0)
12 | ::
13 | +$ state-0 [%0 tome=(mip path:s-p app tome-data) subs=(set path)] :: subs is data paths we are subscribed to
14 | ::
15 | +$ card card:agent:gall
16 | --
17 | ::
18 | %+ verb &
19 | %- agent:dbug
20 | =| state-0
21 | =* state -
22 | ::
23 | ^- agent:gall
24 | ::
25 | =<
26 | |_ =bowl:gall
27 | +* this .
28 | def ~(. (defa this %|) bowl)
29 | eng ~(. +> [bowl ~])
30 | ++ on-init
31 | ^- (quip card _this)
32 | ~> %bout.[0 '%tome +on-init']
33 | =^ cards state
34 | abet:init:eng
35 | [cards this]
36 | ::
37 | ++ on-save
38 | ^- vase
39 | ~> %bout.[0 '%tome +on-save']
40 | !>(state)
41 | ::
42 | ++ on-load
43 | |= ole=vase
44 | ~> %bout.[0 '%tome +on-load']
45 | ^- (quip card _this)
46 | =^ cards state
47 | abet:(load:eng ole)
48 | [cards this]
49 | ::
50 | ++ on-poke
51 | |= cag=cage
52 | ~> %bout.[0 '%tome +on-poke']
53 | ^- (quip card _this)
54 | =^ cards state abet:(poke:eng cag)
55 | [cards this]
56 | ::
57 | ++ on-peek
58 | |= =path
59 | ~> %bout.[0 '%tome +on-peek']
60 | ^- (unit (unit cage))
61 | (peek:eng path)
62 | ::
63 | ++ on-agent
64 | |= [pol=(pole knot) sig=sign:agent:gall]
65 | ~> %bout.[0 '%tome +on-agent']
66 | ^- (quip card _this)
67 | =^ cards state abet:(dude:eng pol sig)
68 | [cards this]
69 | ::
70 | ++ on-arvo
71 | |= [wir=wire sig=sign-arvo]
72 | ~> %bout.[0 '%tome +on-arvo']
73 | ^- (quip card _this)
74 | `this
75 | ::
76 | ++ on-watch
77 | |= =path
78 | ~> %bout.[0 '%tome +on-watch']
79 | ^- (quip card _this)
80 | =^ cards state abet:(watch:eng path)
81 | [cards this]
82 | ::
83 | ++ on-fail
84 | ~> %bout.[0 '%tome +on-fail']
85 | on-fail:def
86 | ::
87 | ++ on-leave
88 | ~> %bout.[0 '%tome +on-leave']
89 | on-leave:def
90 | --
91 | |_ [bol=bowl:gall dek=(list card)]
92 | +* dat .
93 | ++ emit |=(=card dat(dek [card dek]))
94 | ++ emil |=(lac=(list card) dat(dek (welp lac dek)))
95 | ++ abet
96 | ^- (quip card _state)
97 | [(flop dek) state]
98 | ::
99 | ++ init
100 | ^+ dat
101 | dat
102 | ::
103 | ++ load
104 | |= vaz=vase
105 | ^+ dat
106 | ?> ?=([%0 *] q.vaz)
107 | dat(state !<(state-0 vaz))
108 | :: +watch: handle on-watch
109 | ::
110 | ++ watch
111 | |= pol=(pole knot)
112 | ^+ dat
113 | ?+ pol ~|(bad-watch-path/pol !!)
114 | [%kv ship=@ space=@ app=@ bucket=@ rest=*]
115 | =/ ship `@p`(slav %p ship.pol)
116 | =/ space (woad space.pol)
117 | =^ cards state
118 | kv-abet:(kv-watch:(kv-abed:kv [ship space app.pol bucket.pol]) rest.pol)
119 | (emil cards)
120 | ::
121 | [%feed ship=@ space=@ app=@ bucket=@ log=@ rest=*]
122 | =/ ship `@p`(slav %p ship.pol)
123 | =/ space (woad space.pol)
124 | =/ log=? =(log.pol 'log')
125 | =^ cards state
126 | fe-abet:(fe-watch:(fe-abed:fe [ship space app.pol bucket.pol log]) rest.pol)
127 | (emil cards)
128 | ==
129 | :: +dude: handle on-agent
130 | ::
131 | ++ dude
132 | |= [pol=(pole knot) sig=sign:agent:gall]
133 | ^+ dat
134 | =^ cards state
135 | ?+ pol ~|(bad-dude-wire/pol !!)
136 | [%kv ship=@ space=@ app=@ bucket=@ rest=*]
137 | ::
138 | =/ ship `@p`(slav %p ship.pol)
139 | ?+ -.sig `state
140 | %fact kv-abet:(kv-dude:(kv-abed:kv [ship space.pol app.pol bucket.pol]) cage.sig)
141 | ::
142 | %kick
143 | =. subs (~(del in subs) pol)
144 | kv-abet:(kv-view:(kv-abed:kv [ship space.pol app.pol bucket.pol]) rest.pol)
145 | ::
146 | %watch-ack
147 | ?+ rest.pol ~|(bad-kv-watch-ack-path/rest.pol !!)
148 | [%data %all ~]
149 | ?~ p.sig
150 | :: sub success, store that we're subscribed
151 | =. subs (~(put in subs) pol)
152 | `state
153 | ((slog leaf/"kv-watch nack" ~) `state)
154 | ::
155 | [%perm ~]
156 | %. `state
157 | ?~(p.sig same (slog leaf/"kv-watch nack" ~))
158 | ::
159 | ==
160 | ::
161 | ==
162 | [%feed ship=@ space=@ app=@ bucket=@ log=@ rest=*]
163 | ::
164 | =/ ship `@p`(slav %p ship.pol)
165 | =/ log=? =(log.pol 'log')
166 | ?+ -.sig `state
167 | %fact fe-abet:(fe-dude:(fe-abed:fe [ship space.pol app.pol bucket.pol log]) cage.sig)
168 | ::
169 | %kick
170 | =. subs (~(del in subs) pol)
171 | fe-abet:(fe-view:(fe-abed:fe [ship space.pol app.pol bucket.pol log]) rest.pol)
172 | ::
173 | %watch-ack
174 | ?+ rest.pol ~|(bad-feed-watch-ack-path/rest.pol !!)
175 | [%data %all ~]
176 | ?~ p.sig
177 | =. subs (~(put in subs) pol)
178 | `state
179 | ((slog leaf/"feed-watch nack" ~) `state)
180 | ::
181 | [%perm ~]
182 | %. `state
183 | ?~(p.sig same (slog leaf/"feed-watch nack" ~))
184 | ::
185 | ==
186 | ==
187 | ::
188 | ==
189 | (emil cards)
190 | :: +poke: handle on-poke
191 | ::
192 | ++ poke
193 | |= [mar=mark vaz=vase]
194 | ^+ dat
195 | =^ cards state
196 | ?+ mar ~|(bad-tome-mark/mar !!)
197 | %tome-action
198 | =/ act !<(tome-action vaz)
199 | =/ ship `@p`(slav %p ship.act)
200 | ?- -.act
201 | %init-tome
202 | ?. =(our.bol src.bol) ~|('no-foreign-init-tome' !!)
203 | ?: (~(has bi tome) [ship space.act] app.act)
204 | `state
205 | `state(tome (~(put bi tome) [ship space.act] app.act *tome-data))
206 | ::
207 | :: the following init pokes expect an %init-tome to already have been done.
208 | %init-kv
209 | ?. =(our.bol src.bol) ~|('no-foreign-init-kv' !!)
210 | =+ tod=(~(got bi tome) [ship space.act] app.act)
211 | ?: (~(has by store.tod) bucket.act)
212 | `state
213 | =. store.tod (~(put by store.tod) bucket.act [perm.act *invited *kv-meta *kv-data])
214 | `state(tome (~(put bi tome) [ship space.act] app.act tod))
215 | ::
216 | %init-feed
217 | ?. =(our.bol src.bol) ~|('no-foreign-init-feed' !!)
218 | =+ tod=(~(got bi tome) [ship space.act] app.act)
219 | ?: (~(has by feed.tod) [bucket.act log.act])
220 | `state
221 | =. feed.tod (~(put by feed.tod) [bucket.act log.act] [perm.act *feed-ids *invited *feed-data])
222 | `state(tome (~(put bi tome) [ship space.act] app.act tod))
223 | ::
224 | ==
225 | ::
226 | %kv-action
227 | =/ act !<(kv-action vaz)
228 | =/ ship `@p`(slav %p ship.act)
229 | =* do kv-abet:(kv-poke:(kv-abed:kv [ship space.act app.act bucket.act]) act)
230 | ?- -.act
231 | %set-value do
232 | %remove-value do
233 | %clear-kv do
234 | %verify-kv do
235 | %perm-kv
236 | ?. =(our.bol ship) ~|('no-perm-foreign-kv' !!)
237 | do
238 | %invite-kv
239 | ?. =(our.bol ship) ~|('no-invite-foreign-kv' !!)
240 | do
241 | %team-kv
242 | ?: =(our.bol ship) ~|('no-team-local-kv' !!)
243 | kv-abet:(kv-view:(kv-abed:kv [ship space.act app.act bucket.act]) [%perm ~])
244 | %watch-kv
245 | ?: =(our.bol ship) ~|('no-watch-local-kv' !!)
246 | kv-abet:(kv-view:(kv-abed:kv [ship space.act app.act bucket.act]) [%data %all ~])
247 | ==
248 | ::
249 | %feed-action
250 | =/ act !<(feed-action vaz)
251 | =/ ship `@p`(slav %p ship.act)
252 | =* do fe-abet:(fe-poke:(fe-abed:fe [ship space.act app.act bucket.act log.act]) act)
253 | ?- -.act
254 | %new-post do
255 | %delete-post do
256 | %edit-post do
257 | %clear-feed do
258 | %verify-feed do
259 | %set-post-link do
260 | %remove-post-link do
261 | %perm-feed
262 | ?. =(our.bol ship) ~|('no-perm-foreign-feed' !!)
263 | do
264 | %invite-feed
265 | ?. =(our.bol ship) ~|('no-invite-foreign-feed' !!)
266 | do
267 | %team-feed
268 | ?: =(our.bol ship) ~|('no-team-local-feed' !!)
269 | fe-abet:(fe-view:(fe-abed:fe [ship space.act app.act bucket.act log.act]) [%perm ~])
270 | %watch-feed
271 | ?: =(our.bol ship) ~|('no-watch-local-feed' !!)
272 | fe-abet:(fe-view:(fe-abed:fe [ship space.act app.act bucket.act log.act]) [%data %all ~])
273 | ==
274 | ==
275 | (emil cards)
276 | :: +peek: handle on-peek
277 | ::
278 | ++ peek
279 | |= pol=(pole knot)
280 | ^- (unit (unit cage))
281 | ?+ pol ~|(bad-tome-peek-path/pol !!)
282 | [%x %kv ship=@ space=@ app=@ bucket=@ rest=*]
283 | =/ ship `@p`(slav %p ship.pol)
284 | =/ space (woad space.pol)
285 | (kv-peek:(kv-abed:kv [ship space app.pol bucket.pol]) rest.pol)
286 | ::
287 | [%x %feed ship=@ space=@ app=@ bucket=@ log=@ rest=*]
288 | =/ ship `@p`(slav %p ship.pol)
289 | =/ space (woad space.pol)
290 | =/ log=? =(log.pol 'log')
291 | (fe-peek:(fe-abed:fe [ship space app.pol bucket.pol log]) rest.pol)
292 | ::
293 | ==
294 | ::
295 | :: +kv: keyvalue engine
296 | ::
297 | ++ kv
298 | |_ $: shi=ship
299 | spa=space
300 | =app
301 | buc=bucket
302 | tod=tome-data
303 | per=perm
304 | inv=invited
305 | meta=kv-meta
306 | data=kv-data
307 | caz=(list card)
308 | data-pax=path
309 | perm-pax=path
310 | ==
311 | +* kv .
312 | ++ kv-emit |=(c=card kv(caz [c caz]))
313 | ++ kv-emil |=(lc=(list card) kv(caz (welp lc caz)))
314 | ++ kv-abet
315 | ^- (quip card _state)
316 | =. store.tod (~(put by store.tod) buc [per inv meta data])
317 | [(flop caz) state(tome (~(put bi tome) [shi spa] app tod))]
318 | :: +kv-abed: initialize nested core. only works when the map entries already exist
319 | ::
320 | ++ kv-abed
321 | |= [p=ship s=space a=^app b=bucket]
322 | =/ tod (~(got bi tome) [p s] a)
323 | =/ sto (~(got by store.tod) b)
324 | =/ pp `@tas`(scot %p p)
325 | =/ suv (wood s)
326 | %= kv
327 | shi p
328 | spa s
329 | app a
330 | buc b
331 | tod tod
332 | per perm.sto
333 | inv invites.sto
334 | meta meta.sto
335 | data data.sto
336 | data-pax /kv/[pp]/[suv]/[a]/[b]/data/all
337 | perm-pax /kv/[pp]/[suv]/[a]/[b]/perm
338 | ==
339 | :: +kv-dude: handle foreign kv updates (facts)
340 | ::
341 | ++ kv-dude
342 | |= cag=cage
343 | ^+ kv
344 | ?< =(our.bol shi)
345 | ?+ p.cag ~|('bad-kv-dude' !!)
346 | %kv-update
347 | =/ upd !<(kv-update q.cag)
348 | ?+ -.upd ~|('bad-kv-update' !!)
349 | %set
350 | %= kv
351 | data (~(put by data) key.upd s+value.upd)
352 | caz [[%give %fact ~[data-pax] %kv-update !>(upd)] caz]
353 | ==
354 | ::
355 | %remove
356 | %= kv
357 | data (~(del by data) key.upd)
358 | caz [[%give %fact ~[data-pax] %kv-update !>(upd)] caz]
359 | ==
360 | ::
361 | %clear
362 | %= kv
363 | data *kv-data
364 | caz [[%give %fact ~[data-pax] %kv-update !>(upd)] caz]
365 | ==
366 | ::
367 | %all
368 | %= kv
369 | data data.upd
370 | caz [[%give %fact ~[data-pax] %kv-update !>(upd)] caz]
371 | ==
372 | ::
373 | %perm
374 | =/ lc :~ [%give %fact ~[perm-pax] %kv-update !>(upd)]
375 | [%pass perm-pax %agent [shi %tome] %leave ~]
376 | ==
377 | %= kv
378 | per [read=%yes +.upd]
379 | caz (welp lc caz)
380 | ==
381 | ::
382 | ==
383 | ==
384 | :: +kv-watch: handle incoming kv watch requests
385 | ::
386 | ++ kv-watch
387 | |= rest=(pole knot)
388 | ^+ kv
389 | ?> (kv-perm %read)
390 | ?+ rest ~|(bad-kv-watch-path/rest !!)
391 | [%perm ~]
392 | %- kv-emit
393 | [%give %fact ~ %kv-update !>(`kv-update`kv-team)]
394 | ::
395 | [%data %all ~]
396 | %- kv-emit
397 | [%give %fact ~ %kv-update !>(`kv-update`[%all data])]
398 | ::
399 | ==
400 | :: +kv-poke: handle kv poke requests
401 | :: cm = current metadata
402 | :: nm = new metadata
403 | ::
404 | ++ kv-poke
405 | |= act=kv-action
406 | ^+ kv
407 | :: right now live updates only go to the subscribeAll endpoint
408 | ?+ -.act ~|('bad-kv-action' !!)
409 | %set-value
410 | =+ cm=(~(gut by meta) key.act ~)
411 | =* lvl
412 | ?~ cm
413 | %create
414 | ?:(=(src.bol created-by.cm) %create %overwrite)
415 | ?> (kv-perm lvl)
416 | :: equivalent value is already set, do nothing.
417 | ?: =(s+value.act (~(gut by data) key.act ~)) kv
418 | ::
419 | =/ nm
420 | ?~ cm
421 | :: this value is new, so create new metadata entry alongside it
422 | [src.bol src.bol now.bol now.bol]
423 | :: this value already exists, so update its metadata
424 | [created-by.cm src.bol created-at.cm now.bol]
425 | ::
426 | %= kv
427 | meta (~(put by meta) key.act nm)
428 | data (~(put by data) key.act s+value.act)
429 | caz [[%give %fact ~[data-pax] %kv-update !>(`kv-update`[%set key.act value.act])] caz]
430 | ==
431 | ::
432 | %remove-value
433 | =+ cm=(~(gut by meta) key.act ~)
434 | =* lvl
435 | ?~ cm
436 | %create
437 | ?:(=(src.bol created-by.cm) %create %overwrite)
438 | ?> (kv-perm lvl)
439 | :: value doesn't exist, do nothing
440 | ?~ cm
441 | kv
442 | ::
443 | %= kv
444 | meta (~(del by meta) key.act)
445 | data (~(del by data) key.act)
446 | caz [[%give %fact ~[data-pax] %kv-update !>(`kv-update`[%remove key.act])] caz]
447 | ==
448 | ::
449 | %clear-kv
450 | :: could check if all values are ours for %create perm level, but that's overkill
451 | ?> (kv-perm %overwrite)
452 | ?~ meta kv :: nothing to clear
453 | %= kv
454 | meta *kv-meta
455 | data *kv-data
456 | caz [[%give %fact ~[data-pax] %kv-update !>(`kv-update`[%clear ~])] caz]
457 | ==
458 | ::
459 | %verify-kv
460 | :: The bucket must exist to get this far, so we just need to verify read permissions.
461 | ?> (kv-perm %read)
462 | kv
463 | ::
464 | %perm-kv
465 | :: force everyone to re-subscribe
466 | kv(per perm.act, caz [[%give %kick ~[data-pax] ~] caz])
467 | ::
468 | %invite-kv
469 | =/ guy `@p`(slav %p guy.act)
470 | %= kv
471 | inv (~(put by inv) guy level.act)
472 | caz [[%give %kick ~[data-pax] `guy] caz]
473 | ==
474 | ::
475 | ==
476 | :: +kv-peek: handle kv peek requests
477 | ::
478 | ++ kv-peek
479 | |= rest=(pole knot)
480 | ^- (unit (unit cage))
481 | :: no perms check since no remote scry
482 | ?+ rest ~|(bad-kv-peek-path/rest !!)
483 | [%data %all ~]
484 | ``kv-update+!>(`kv-update`[%all data])
485 | ::
486 | [%data %key key=@t ~]
487 | ``kv-update+!>(`kv-update`[%get (~(gut by data) key.rest ~)])
488 | ::
489 | ==
490 | :: +kv-view: start watching foreign kv (permissions or path)
491 | ::
492 | ++ kv-view
493 | |= rest=(pole knot)
494 | ^+ kv
495 | ?+ rest ~|(bad-kv-watch-path/rest !!)
496 | [%perm ~]
497 | (kv-emit [%pass perm-pax %agent [shi %tome] %watch perm-pax])
498 | ::
499 | [%data %all ~]
500 | ?: (~(has in subs) data-pax) kv
501 | (kv-emit [%pass data-pax %agent [shi %tome] %watch data-pax])
502 | ::
503 | ==
504 | :: +kv-perm: check a permission level, return true if allowed
505 | ::
506 | ++ kv-perm
507 | |= [lvl=?(%read %create %overwrite)]
508 | ^- ?
509 | ?: =(src.bol our.bol) %.y :: always allow local
510 | =/ bro (~(gut by inv) src.bol ~)
511 | ?- lvl
512 | %read
513 | ?~ bro
514 | ?- read.per
515 | %unset %.n
516 | %no %.n
517 | %our %.n :: it's not us, so no.
518 | %open %.y
519 | %yes %.y
520 | %space
521 | =/ memb .^(view:m-s:r-l %gx /(scot %p our.bol)/spaces/(scot %da now.bol)/(scot %p shi)/[spa]/is-member/(scot %p our.bol)/noun)
522 | ?> ?=(%is-member -.memb)
523 | is-member.memb
524 | ==
525 | :: use invite level to determine
526 | ?:(?=(%block bro) %.n %.y)
527 | ::
528 | %create
529 | ?~ bro
530 | ?- write.per
531 | %unset %.n
532 | %no %.n
533 | %our %.n
534 | %open %.y
535 | %yes %.y
536 | %space
537 | =/ memb .^(view:m-s:r-l %gx /(scot %p our.bol)/spaces/(scot %da now.bol)/(scot %p shi)/[spa]/is-member/(scot %p our.bol)/noun)
538 | ?> ?=(%is-member -.memb)
539 | is-member.memb
540 | ==
541 | ?:(?=(?(%block %read) bro) %.n %.y)
542 | ::
543 | %overwrite
544 | ?~ bro
545 | ?- admin.per
546 | %unset %.n
547 | %no %.n
548 | %our %.n
549 | %open %.y
550 | %yes %.y
551 | %space
552 | =/ memb .^(view:m-s:r-l %gx /(scot %p our.bol)/spaces/(scot %da now.bol)/(scot %p shi)/[spa]/is-member/(scot %p our.bol)/noun)
553 | ?> ?=(%is-member -.memb)
554 | is-member.memb
555 | ==
556 | ?:(?=(?(%block %read %write) bro) %.n %.y)
557 | ::
558 | ==
559 | :: +kv-team: get write/admin permissions for a ship
560 | ::
561 | ++ kv-team
562 | =/ write ?:((kv-perm %create) %yes %no)
563 | =/ admin ?:((kv-perm %overwrite) %yes %no)
564 | [%perm write admin]
565 | ::
566 | --
567 | ::
568 | :: +fe: feed engine
569 | ::
570 | ++ fe
571 | |_ $: shi=ship
572 | spa=space
573 | ap=app
574 | buc=bucket
575 | lo=log
576 | tod=tome-data
577 | per=perm
578 | ids=feed-ids
579 | inv=invited
580 | data=feed-data
581 | caz=(list card)
582 | data-pax=path
583 | perm-pax=path
584 | ==
585 | +* fe .
586 | ++ fe-emit |=(c=card fe(caz [c caz]))
587 | ++ fe-emil |=(lc=(list card) fe(caz (welp lc caz)))
588 | ++ fe-abet
589 | ^- (quip card _state)
590 | =. feed.tod (~(put by feed.tod) [buc lo] [per ids inv data])
591 | [(flop caz) state(tome (~(put bi tome) [shi spa] ap tod))]
592 | :: +kv-abed: initialize nested core. only works when the map entries already exist
593 | ::
594 | ++ fe-abed
595 | |= [p=ship s=space a=app b=bucket l=log]
596 | =/ tod (~(got bi tome) [p s] a)
597 | =/ fee (~(got by feed.tod) [b l])
598 | =/ pp `@tas`(scot %p p)
599 | =/ suv (wood s)
600 | =/ type ?:(=(l %.y) %log %feed)
601 | %= fe
602 | shi p
603 | spa s
604 | ap a
605 | buc b
606 | lo l
607 | tod tod
608 | per perm.fee
609 | ids ids.fee
610 | inv invites.fee
611 | data data.fee
612 | data-pax /feed/[pp]/[suv]/[a]/[b]/[type]/data/all
613 | perm-pax /feed/[pp]/[suv]/[a]/[b]/[type]/perm
614 | ==
615 | :: +fe-dude: handle foreign feed updates (facts)
616 | ::
617 | ++ fe-dude
618 | |= cag=cage
619 | ^+ fe
620 | ?< =(our.bol shi)
621 | ?+ p.cag ~|('bad-feed-dude' !!)
622 | %feed-update
623 | =/ upd !<(feed-update q.cag)
624 | =/ fon ((on time feed-value) gth) :: mop needs this to work
625 | ?+ -.upd ~|('bad-feed-update' !!)
626 | %new
627 | %= fe
628 | ids (~(put by ids) id.upd time.upd)
629 | data (put:fon data time.upd [id.upd ship.upd ship.upd time.upd time.upd s+content.upd *links])
630 | caz [[%give %fact ~[data-pax] %feed-update !>(upd)] caz]
631 | ==
632 | ::
633 | %edit
634 | =/ has (~(has by ids) id.upd)
635 | =/ new-ids :: add new ID if we don't have original
636 | ?: has
637 | ids
638 | (~(put by ids) id.upd time.upd)
639 | ::
640 | =/ og-time (~(got by ids) id.upd)
641 | ::
642 | =/ old-by
643 | ?: has
644 | created-by:(got:fon data og-time)
645 | :: if we receive %edit without an original, just use them as the original author.
646 | ship.upd
647 | ::
648 | %= fe
649 | ids new-ids
650 | data (put:fon data og-time [id.upd old-by ship.upd og-time time.upd s+content.upd *links])
651 | caz [[%give %fact ~[data-pax] %feed-update !>(upd)] caz]
652 | ==
653 | ::
654 | %delete
655 | :: don't have it, ignore
656 | ?. (~(has by ids) id.upd) fe
657 | =/ res (del:fon data time.upd)
658 | %= fe
659 | ids (~(del by ids) id.upd)
660 | data +.res
661 | caz [[%give %fact ~[data-pax] %feed-update !>(upd)] caz]
662 | ==
663 | ::
664 | %clear
665 | %= fe
666 | ids *feed-ids
667 | data *feed-data
668 | caz [[%give %fact ~[data-pax] %feed-update !>(upd)] caz]
669 | ==
670 | ::
671 | %set-link
672 | :: don't have the post, ignore
673 | ?. (~(has by ids) id.upd) fe
674 | =/ post (got:fon data time.upd)
675 | =. links.post (~(put by links.post) ship.upd s+value.upd)
676 | %= fe
677 | data (put:fon data time.upd post)
678 | caz [[%give %fact ~[data-pax] %feed-update !>(upd)] caz]
679 | ==
680 | ::
681 | %remove-link
682 | :: don't have the post, ignore
683 | ?. (~(has by ids) id.upd) fe
684 | =/ post (got:fon data time.upd)
685 | :: don't have the link, ignore
686 | ?. (~(has by links.post) ship.upd) fe
687 | =. links.post (~(del by links.post) ship.upd)
688 | %= fe
689 | data (put:fon data time.upd post)
690 | caz [[%give %fact ~[data-pax] %feed-update !>(upd)] caz]
691 | ==
692 | ::
693 | %all
694 | %= fe
695 | ids (malt (turn (tap:fon data.upd) |=([=time =feed-value] [id.feed-value time])))
696 | data data.upd
697 | caz [[%give %fact ~[data-pax] %feed-update !>(upd)] caz]
698 | ==
699 | ::
700 | %perm
701 | =/ lc :~ [%give %fact ~[perm-pax] %feed-update !>(upd)]
702 | [%pass perm-pax %agent [shi %tome] %leave ~]
703 | ==
704 | %= fe
705 | per [read=%yes +.upd]
706 | caz (welp lc caz)
707 | ==
708 | ::
709 | ==
710 | ==
711 |
712 | :: +fe-watch: handle incoming watch requests
713 | ::
714 | ++ fe-watch
715 | |= rest=(pole knot)
716 | ^+ fe
717 | ?> (fe-perm %read)
718 | ?+ rest ~|(bad-feed-watch-path/rest !!)
719 | [%perm ~]
720 | %- fe-emit
721 | [%give %fact ~ %feed-update !>(`feed-update`fe-team)]
722 | ::
723 | [%data %all ~]
724 | %- fe-emit
725 | [%give %fact ~ %feed-update !>(`feed-update`[%all data])]
726 | ::
727 | ==
728 | :: +fe-poke: handle log/feed pokes
729 | ::
730 | ++ fe-poke
731 | |= act=feed-action
732 | ^+ fe
733 | =/ fon ((on time feed-value) gth) :: mop needs this to work
734 | ?+ -.act ~|('bad-feed-action' !!)
735 | %new-post
736 | ?> (fe-perm %create)
737 | ::
738 | %= fe
739 | ids (~(put by ids) id.act now.bol)
740 | data (put:fon data now.bol [id.act src.bol src.bol now.bol now.bol s+content.act *links])
741 | caz [[%give %fact ~[data-pax] %feed-update !>(`feed-update`[%new id.act now.bol src.bol content.act])] caz]
742 | ==
743 | ::
744 | %edit-post
745 | =+ time=(~(gut by ids) id.act ~)
746 | ?~ time
747 | ?> (fe-perm ?:(=(lo %.y) %overwrite %create))
748 | ~|('no-post-to-edit' !!)
749 | ::
750 | =/ curr (got:fon data time)
751 | =* lvl ?:(=(src.bol created-by.curr) ?:(=(lo %.y) %overwrite %create) %overwrite)
752 | ?> (fe-perm lvl)
753 | ::
754 | %= fe
755 | data (put:fon data time [id.act created-by.curr src.bol created-at.curr now.bol s+content.act *links])
756 | caz [[%give %fact ~[data-pax] %feed-update !>(`feed-update`[%edit id.act now.bol src.bol content.act])] caz]
757 | ==
758 | ::
759 | %delete-post
760 | =+ time=(~(gut by ids) id.act ~)
761 | ?~ time :: if no post, do nothing
762 | ?> (fe-perm ?:(=(lo %.y) %overwrite %create))
763 | fe
764 | ::
765 | =* curr (got:fon data time)
766 | =* lvl ?:(=(src.bol created-by.curr) ?:(=(lo %.y) %overwrite %create) %overwrite)
767 | ?> (fe-perm lvl)
768 | ::
769 | =/ res (del:fon data time)
770 | %= fe
771 | ids (~(del by ids) id.act)
772 | data +.res :: mop delete return type is weird. tail is the new map
773 | caz [[%give %fact ~[data-pax] %feed-update !>(`feed-update`[%delete id.act time])] caz]
774 | ==
775 | ::
776 | %clear-feed
777 | ?> (fe-perm %overwrite)
778 | ?~ ids fe :: if no posts, do nothing
779 | ::
780 | %= fe
781 | ids *feed-ids
782 | data *feed-data
783 | caz [[%give %fact ~[data-pax] %feed-update !>(`feed-update`[%clear ~])] caz]
784 | ==
785 | ::
786 | %verify-feed
787 | :: The bucket must exist to get this far, so we just need to verify read permissions.
788 | ?> (fe-perm %read)
789 | fe
790 | ::
791 | %perm-feed
792 | :: force everyone to re-subscribe
793 | fe(per perm.act, caz [[%give %kick ~[data-pax] ~] caz])
794 | ::
795 | %invite-feed
796 | =/ guy `@p`(slav %p guy.act)
797 | %= fe
798 | inv (~(put by inv) guy level.act)
799 | caz [[%give %kick ~[data-pax] `guy] caz]
800 | ==
801 | ::
802 | %set-post-link :: links are currently only supported by feeds, not logs
803 | ?> =(lo %.n)
804 | ?> (fe-perm %create)
805 | =+ time=(~(gut by ids) id.act ~)
806 | ?~ time ~|('no-post-for-set-link' !!)
807 | ::
808 | =/ curr (got:fon data time)
809 | =/ ship-str `@t`(scot %p src.bol)
810 | =/ new-links (~(put by links.curr) ship-str s+value.act)
811 | %= fe
812 | data (put:fon data time [id.act created-by.curr updated-by.curr created-at.curr updated-at.curr content.curr new-links])
813 | caz [[%give %fact ~[data-pax] %feed-update !>(`feed-update`[%set-link id.act time ship-str value.act])] caz]
814 | ==
815 | ::
816 | %remove-post-link
817 | ?> =(lo %.n)
818 | ?> (fe-perm %create)
819 | =+ time=(~(gut by ids) id.act ~)
820 | ?~ time :: if no post, do nothing.
821 | fe
822 | ::
823 | =/ curr (got:fon data time)
824 | =/ ship-str `@t`(scot %p src.bol)
825 | =/ new-links (~(del by links.curr) ship-str)
826 | %= fe
827 | data (put:fon data time [id.act created-by.curr updated-by.curr created-at.curr updated-at.curr content.curr new-links])
828 | caz [[%give %fact ~[data-pax] %feed-update !>(`feed-update`[%remove-link id.act time ship-str])] caz]
829 | ==
830 | ::
831 | ==
832 | :: +fe-peek: handle kv peek requests
833 | ::
834 | ++ fe-peek
835 | |= rest=(pole knot)
836 | ^- (unit (unit cage))
837 | :: no perms check since no remote scry
838 | ?+ rest ~|(bad-feed-peek-path/rest !!)
839 | [%data %all ~]
840 | ``feed-update+!>(`feed-update`[%all data])
841 | ::
842 | [%data %key id=@t ~]
843 | =/ fon ((on time feed-value) gth)
844 | =/ time (~(gut by ids) id.rest ~)
845 | =/ post
846 | ?~ time ~
847 | (got:fon data time)
848 | ::
849 | ``feed-update+!>(`feed-update`[%get post])
850 | ::
851 | ==
852 | :: fe-view: start watching foreign feed
853 | ::
854 | ++ fe-view
855 | |= rest=(pole knot)
856 | ^+ fe
857 | ?+ rest ~|(bad-feed-watch-path/rest !!)
858 | [%perm ~]
859 | (fe-emit [%pass perm-pax %agent [shi %tome] %watch perm-pax])
860 | ::
861 | [%data %all ~]
862 | ?: (~(has in subs) data-pax) fe
863 | (fe-emit [%pass data-pax %agent [shi %tome] %watch data-pax])
864 | ::
865 | ==
866 | :: +fe-perm: check a permission level, return true if allowed
867 | :: duplicates +kv-perm
868 | ++ fe-perm
869 | |= [lvl=?(%read %create %overwrite)]
870 | ^- ?
871 | ?: =(src.bol our.bol) %.y :: always allow local
872 | =/ bro (~(gut by inv) src.bol ~)
873 | ?- lvl
874 | %read
875 | ?~ bro
876 | ?- read.per
877 | %unset %.n
878 | %no %.n
879 | %our %.n :: it's not us, so no.
880 | %open %.y
881 | %yes %.y
882 | %space
883 | =/ memb .^(view:m-s:r-l %gx /(scot %p our.bol)/spaces/(scot %da now.bol)/(scot %p shi)/[spa]/is-member/(scot %p our.bol)/noun)
884 | ?> ?=(%is-member -.memb)
885 | is-member.memb
886 | ==
887 | :: use invite level to determine
888 | ?:(?=(%block bro) %.n %.y)
889 | ::
890 | %create
891 | ?~ bro
892 | ?- write.per
893 | %unset %.n
894 | %no %.n
895 | %our %.n
896 | %open %.y
897 | %yes %.y
898 | %space
899 | =/ memb .^(view:m-s:r-l %gx /(scot %p our.bol)/spaces/(scot %da now.bol)/(scot %p shi)/[spa]/is-member/(scot %p our.bol)/noun)
900 | ?> ?=(%is-member -.memb)
901 | is-member.memb
902 | ==
903 | ?:(?=(?(%block %read) bro) %.n %.y)
904 | ::
905 | %overwrite
906 | ?~ bro
907 | ?- admin.per
908 | %unset %.n
909 | %no %.n
910 | %our %.n
911 | %open %.y
912 | %yes %.y
913 | %space
914 | =/ memb .^(view:m-s:r-l %gx /(scot %p our.bol)/spaces/(scot %da now.bol)/(scot %p shi)/[spa]/is-member/(scot %p our.bol)/noun)
915 | ?> ?=(%is-member -.memb)
916 | is-member.memb
917 | ==
918 | ?:(?=(?(%block %read %write) bro) %.n %.y)
919 | ::
920 | ==
921 | :: +fe-team: get read/write/admin permissions for a ship
922 | ::
923 | ++ fe-team
924 | =/ write ?:((fe-perm %create) %yes %no)
925 | =/ admin ?:((fe-perm %overwrite) %yes %no)
926 | [%perm write admin]
927 | ::
928 | --
929 | --
--------------------------------------------------------------------------------
/pkg/src/classes/Tome.ts:
--------------------------------------------------------------------------------
1 | import Urbit from '@urbit/http-api'
2 | import {
3 | InviteLevel,
4 | StoreType,
5 | Value,
6 | Content,
7 | SubscribeUpdate,
8 | FeedlogUpdate,
9 | FeedlogEntry,
10 | Perm,
11 | StoreOptions,
12 | TomeOptions,
13 | InitStoreOptions,
14 | } from '../types'
15 | import { tomeMark, kvMark, feedMark, kvThread, feedThread } from './constants'
16 | import { v4 as uuid, validate } from 'uuid'
17 |
18 | export class Tome {
19 | protected api: Urbit
20 | protected mars: boolean
21 |
22 | protected ourShip: string
23 | protected tomeShip: string
24 | protected space: string
25 | protected spaceForPath: string // space name as woad (encoded)
26 | protected app: string
27 | protected agent: string
28 | protected perm: Perm
29 | protected locked: boolean // if true, Tome is locked to the initial ship and space.
30 | protected inRealm: boolean
31 |
32 | protected static async initTomePoke(
33 | api: Urbit,
34 | ship: string,
35 | space: string,
36 | app: string,
37 | agent: string
38 | ) {
39 | await api.poke({
40 | app: agent,
41 | mark: tomeMark,
42 | json: {
43 | 'init-tome': {
44 | ship,
45 | space,
46 | app,
47 | },
48 | },
49 | onError: (error) => {
50 | throw new Error(
51 | `Tome: Initializing Tome on ship ${ship} failed. Make sure the ship and Tome agent are both running.\nError: ${error}`
52 | )
53 | },
54 | })
55 | }
56 |
57 | // maybe use a different (sub) type here?
58 | protected constructor(options?: InitStoreOptions) {
59 | if (typeof options.api !== 'undefined') {
60 | this.mars = true
61 | const {
62 | api,
63 | tomeShip,
64 | ourShip,
65 | space,
66 | spaceForPath,
67 | app,
68 | agent,
69 | perm,
70 | locked,
71 | inRealm,
72 | } = options
73 | this.api = api
74 | this.tomeShip = tomeShip
75 | this.ourShip = ourShip
76 | this.space = space
77 | this.spaceForPath = spaceForPath
78 | this.app = app
79 | this.agent = agent
80 | this.perm = perm
81 | this.locked = locked
82 | this.inRealm = inRealm
83 | } else {
84 | const { app, tomeShip, ourShip } = options ?? {}
85 | this.mars = false
86 | this.app = app
87 | this.tomeShip = tomeShip
88 | this.ourShip = ourShip
89 | }
90 | }
91 |
92 | /**
93 | * @param api The optional Urbit connection to be used for requests.
94 | * @param app An optional app name to store under. Defaults to `'all'`.
95 | * @param options Optional ship, space, agent name, and permissions for initializing a Tome.
96 | * @returns A new Tome instance.
97 | */
98 | static async init(
99 | api?: Urbit,
100 | app?: string,
101 | options: TomeOptions = {}
102 | ): Promise {
103 | const mars = typeof api !== 'undefined'
104 | const appName = app ?? 'all'
105 | if (mars) {
106 | let locked = false
107 | const inRealm = options.realm ?? false
108 | let tomeShip = options.ship ?? api.ship
109 | let space = options.space ?? 'our'
110 | const agent = options.agent ?? 'tome'
111 | let spaceForPath = space
112 |
113 | if (inRealm) {
114 | if (options.ship && options.space) {
115 | locked = true
116 | } else if (!options.ship && !options.space) {
117 | try {
118 | const current = await api.scry({
119 | app: 'spaces',
120 | path: '/current',
121 | })
122 | space = current.current.space
123 |
124 | const path = current.current.path.split('/')
125 | tomeShip = path[1]
126 | spaceForPath = path[2]
127 | } catch (e) {
128 | throw new Error(
129 | 'Tome: no current space found. Make sure Realm is installed / configured, ' +
130 | 'or pass `realm: false` to `Tome.init`.'
131 | )
132 | }
133 | } else {
134 | throw new Error(
135 | 'Tome: `ship` and `space` must neither or both be specified when using Realm.'
136 | )
137 | }
138 | } else {
139 | if (spaceForPath !== 'our') {
140 | throw new Error(
141 | "Tome: only the 'our' space is currently supported when not using Realm. " +
142 | 'If this is needed, please open an issue.'
143 | )
144 | }
145 | }
146 |
147 | if (!tomeShip.startsWith('~')) {
148 | tomeShip = `~${tomeShip}`
149 | }
150 | // save api.ship so we know who we are.
151 | const ourShip = `~${api.ship}`
152 | const perm = options.permissions
153 | ? options.permissions
154 | : ({ read: 'our', write: 'our', admin: 'our' } as const)
155 | await Tome.initTomePoke(api, tomeShip, space, app, agent)
156 | return new Tome({
157 | api,
158 | tomeShip,
159 | ourShip,
160 | space,
161 | spaceForPath,
162 | app: appName,
163 | agent,
164 | perm,
165 | locked,
166 | inRealm,
167 | })
168 | }
169 | return new Tome({ app: appName, tomeShip: 'zod', ourShip: 'zod' })
170 | }
171 |
172 | private async _initStore(
173 | options: StoreOptions,
174 | type: StoreType,
175 | isLog: boolean
176 | ) {
177 | let permissions = options.permissions ? options.permissions : this.perm
178 | if (this.app === 'all' && this.isOurStore()) {
179 | console.warn(
180 | `Tome-${type}: Permissions on 'all' are ignored. Setting permissions levels to 'our' instead...`
181 | )
182 | permissions = {
183 | read: 'our',
184 | write: 'our',
185 | admin: 'our',
186 | } as const
187 | }
188 | return await DataStore.initDataStore({
189 | type,
190 | api: this.api,
191 | tomeShip: this.tomeShip,
192 | ourShip: this.ourShip,
193 | space: this.space,
194 | spaceForPath: this.spaceForPath,
195 | app: this.app,
196 | agent: this.agent,
197 | perm: permissions,
198 | locked: this.locked,
199 | bucket: options.bucket ?? 'def',
200 | preload: options.preload ?? true,
201 | onReadyChange: options.onReadyChange,
202 | onLoadChange: options.onLoadChange,
203 | onWriteChange: options.onWriteChange,
204 | onAdminChange: options.onAdminChange,
205 | onDataChange: options.onDataChange,
206 | isLog,
207 | inRealm: this.inRealm,
208 | })
209 | }
210 |
211 | /**
212 | * Initialize or connect to a key-value store.
213 | *
214 | * @param options Optional bucket, permissions, preload flag, and callbacks for the store. `permisssions`
215 | * defaults to the Tome's permissions, `bucket` to `'def'`, and `preload` to `true`.
216 | * @returns A `KeyValueStore`.
217 | */
218 | public async keyvalue(options: StoreOptions = {}): Promise {
219 | if (this.mars) {
220 | return (await this._initStore(
221 | options,
222 | 'kv',
223 | false
224 | )) as KeyValueStore
225 | }
226 | return new KeyValueStore({
227 | app: this.app,
228 | bucket: options.bucket ?? 'def',
229 | preload: options.preload ?? true,
230 | onDataChange: options.onDataChange,
231 | onLoadChange: options.onLoadChange,
232 | type: 'kv',
233 | })
234 | }
235 |
236 | /**
237 | * Initialize or connect to a feed store.
238 | *
239 | * @param options Optional bucket, permissions, preload flag, and callbacks for the feed. `permisssions`
240 | * defaults to the Tome's permissions, `bucket` to `'def'`, and `preload` to `true`.
241 | * @returns A `FeedStore`.
242 | */
243 | public async feed(options: StoreOptions = {}): Promise {
244 | if (this.mars) {
245 | return (await this._initStore(options, 'feed', false)) as FeedStore
246 | }
247 | return new FeedStore({
248 | app: this.app,
249 | bucket: options.bucket ?? 'def',
250 | preload: options.preload ?? true,
251 | onDataChange: options.onDataChange,
252 | onLoadChange: options.onLoadChange,
253 | isLog: false,
254 | type: 'feed',
255 | ourShip: 'zod',
256 | })
257 | }
258 |
259 | /**
260 | * Initialize or connect to a log store.
261 | *
262 | * @param options Optional bucket, permissions, preload flag, and callbacks for the log. `permisssions`
263 | * defaults to the Tome's permissions, `bucket` to `'def'`, and `preload` to `true`.
264 | * @returns A `LogStore`.
265 | */
266 | public async log(options: StoreOptions = {}): Promise {
267 | if (this.mars) {
268 | return (await this._initStore(options, 'feed', true)) as LogStore
269 | }
270 | return new LogStore({
271 | app: this.app,
272 | bucket: options.bucket ?? 'def',
273 | preload: options.preload ?? true,
274 | onDataChange: options.onDataChange,
275 | onLoadChange: options.onLoadChange,
276 | isLog: true,
277 | type: 'feed',
278 | ourShip: 'zod',
279 | })
280 | }
281 |
282 | public isOurStore(): boolean {
283 | return this.tomeShip === this.ourShip
284 | }
285 | }
286 |
287 | export abstract class DataStore extends Tome {
288 | protected storeSubscriptionID: number
289 | protected spaceSubscriptionID: number
290 | // if preload is set, loaded will be set to true once the initial subscription state has been received.
291 | // then we know we can use the cache.
292 | protected preload: boolean
293 | protected loaded: boolean
294 | protected ready: boolean // if false, we are switching spaces.
295 | protected onReadyChange: (ready: boolean) => void
296 | protected onLoadChange: (loaded: boolean) => void
297 | protected onWriteChange: (write: boolean) => void
298 | protected onAdminChange: (admin: boolean) => void
299 | protected onDataChange: (data: any) => void
300 |
301 | protected cache: Map // cache key-value pairs
302 | protected feedlog: FeedlogEntry[] // array of objects (feed entries)
303 | protected order: string[] // ids of feed entries in order
304 |
305 | protected bucket: string
306 | protected write: boolean
307 | protected admin: boolean
308 |
309 | protected type: StoreType
310 | protected isLog: boolean
311 |
312 | // assumes MARZ
313 | public static async initDataStore(
314 | options: InitStoreOptions
315 | ): Promise {
316 | const { tomeShip, ourShip, type, isLog } = options
317 | if (tomeShip === ourShip) {
318 | await DataStore.initBucket(options)
319 | const newOptions = { ...options, write: true, admin: true }
320 | switch (type) {
321 | case 'kv':
322 | return new KeyValueStore(newOptions)
323 | case 'feed':
324 | if (isLog) {
325 | return new LogStore(options)
326 | } else {
327 | return new FeedStore(options)
328 | }
329 | }
330 | }
331 | await DataStore.checkExistsAndCanRead(options)
332 | const foreignPerm = {
333 | read: 'yes',
334 | write: 'unset',
335 | admin: 'unset',
336 | } as const
337 | await DataStore.initBucket({ ...options, perm: foreignPerm })
338 | await DataStore.startWatchingForeignBucket(options)
339 | await DataStore._getCurrentForeignPerms(options)
340 | switch (type) {
341 | case 'kv':
342 | return new KeyValueStore(options)
343 | case 'feed':
344 | if (isLog) {
345 | return new LogStore(options)
346 | } else {
347 | return new FeedStore(options)
348 | }
349 | }
350 | }
351 |
352 | constructor(options?: InitStoreOptions) {
353 | super(options)
354 | const {
355 | bucket,
356 | write,
357 | admin,
358 | preload,
359 | onReadyChange,
360 | onLoadChange,
361 | onWriteChange,
362 | onAdminChange,
363 | onDataChange,
364 | type,
365 | isLog,
366 | } = options ?? {}
367 | this.bucket = bucket
368 | this.preload = preload
369 | this.type = type
370 | this.write = write
371 | this.admin = admin
372 | this.onDataChange = onDataChange
373 | this.onLoadChange = onLoadChange
374 | this.onReadyChange = onReadyChange
375 | this.onWriteChange = onWriteChange
376 | this.onAdminChange = onAdminChange
377 | this.cache = new Map()
378 | this.feedlog = []
379 | this.order = []
380 | if (preload) {
381 | this.setLoaded(false)
382 | }
383 | if (type === 'feed') {
384 | this.isLog = isLog
385 | } else {
386 | this.isLog = false
387 | }
388 | if (this.mars) {
389 | if (preload) {
390 | this.subscribeAll()
391 | }
392 | if (this.inRealm) {
393 | this.watchCurrentSpace()
394 | }
395 | this.watchPerms()
396 | this.setReady(true)
397 | } else {
398 | if (preload) {
399 | this.getAllLocalValues()
400 | }
401 | }
402 | }
403 |
404 | private async watchPerms(): Promise {
405 | await this.api.subscribe({
406 | app: this.agent,
407 | path: this.permsPath(),
408 | err: () => {
409 | console.error(
410 | `Tome-${this.type}: unable to watch perms for this bucket.`
411 | )
412 | },
413 | event: async (perms: Perm) => {
414 | if (perms.write !== 'unset') {
415 | const write = perms.write === 'yes'
416 | this.setWrite(write)
417 | }
418 | if (perms.admin !== 'unset') {
419 | const admin = perms.admin === 'yes'
420 | this.setAdmin(admin)
421 | }
422 | },
423 | quit: async () => await this.watchPerms(),
424 | })
425 | }
426 |
427 | private async watchCurrentSpace(): Promise {
428 | this.spaceSubscriptionID = await this.api.subscribe({
429 | app: 'spaces',
430 | path: '/current',
431 | err: () => {
432 | throw new Error(
433 | `Tome-${this.type}: unable to watch current space in spaces agent. Is Realm installed and configured?`
434 | )
435 | },
436 | event: async (current: {
437 | current: { path: string; space: string }
438 | }) => {
439 | const space = current.current.space
440 | const path = current.current.path.split('/')
441 | const tomeShip = path[1]
442 | const spaceForPath = path[2]
443 | if (tomeShip !== this.tomeShip || space !== this.space) {
444 | if (this.locked) {
445 | throw new Error(
446 | `Tome-${this.type}: spaces cannot be switched while using a locked Tome.`
447 | )
448 | }
449 | await this._wipeAndChangeSpace(
450 | tomeShip,
451 | space,
452 | spaceForPath
453 | )
454 | }
455 | },
456 | quit: async () => await this.watchCurrentSpace(),
457 | })
458 | }
459 |
460 | // this seems like pretty dirty update method, is there a better way?
461 | private async _wipeAndChangeSpace(
462 | tomeShip: string,
463 | space: string,
464 | spaceForPath: string
465 | ): Promise {
466 | this.setReady(false)
467 | if (this.storeSubscriptionID) {
468 | await this.api.unsubscribe(this.storeSubscriptionID)
469 | }
470 | // changing the top level tome, so we reinitialize
471 | await Tome.initTomePoke(this.api, tomeShip, space, this.app, this.agent)
472 | const perm =
473 | tomeShip === this.ourShip
474 | ? this.perm
475 | : ({ read: 'yes', write: 'unset', admin: 'unset' } as const)
476 |
477 | const options = {
478 | api: this.api,
479 | tomeShip,
480 | space,
481 | app: this.app,
482 | agent: this.agent,
483 | bucket: this.bucket,
484 | type: this.type,
485 | isLog: this.isLog,
486 | perm,
487 | }
488 | // if not ours, we need to make sure we have read access first.
489 | if (tomeShip !== this.ourShip) {
490 | await DataStore.checkExistsAndCanRead(options)
491 | }
492 | // that succeeded, whether ours or not initialize the bucket.
493 | await DataStore.initBucket(options)
494 | // if not us, we want Hoon side to start a subscription.
495 | if (tomeShip !== this.ourShip) {
496 | await DataStore.startWatchingForeignBucket(options)
497 | }
498 |
499 | this.tomeShip = tomeShip
500 | this.space = space
501 | this.spaceForPath = spaceForPath
502 | this.wipeLocalValues()
503 | if (this.preload) {
504 | this.setLoaded(false)
505 | await this.subscribeAll()
506 | }
507 |
508 | if (this.isOurStore()) {
509 | this.write = true
510 | this.admin = true
511 | } else {
512 | await this.watchPerms()
513 | }
514 | this.setReady(true)
515 | }
516 |
517 | protected static async checkExistsAndCanRead(
518 | options: InitStoreOptions
519 | ): Promise {
520 | const { api, tomeShip, space, app, agent, bucket, type, isLog } =
521 | options
522 | const action = `verify-${type}`
523 | const body = {
524 | [action]: {
525 | ship: tomeShip,
526 | space,
527 | app,
528 | bucket,
529 | },
530 | }
531 | if (type === 'feed') {
532 | // @ts-expect-error
533 | body[action].log = isLog
534 | }
535 | // Tunnel poke to Tome ship
536 | try {
537 | const result = await api.thread({
538 | inputMark: 'json',
539 | outputMark: 'json',
540 | threadName: `${type}-poke-tunnel`,
541 | body: {
542 | ship: tomeShip,
543 | agent: agent,
544 | json: JSON.stringify(body),
545 | },
546 | desk: agent,
547 | })
548 | const success = result === 'success'
549 | if (!success) {
550 | throw new Error(
551 | `Tome-${type}: the requested bucket does not exist, or you do not have permission to access it.`
552 | )
553 | }
554 | } catch (e) {
555 | throw new Error(
556 | `Tome-${type}: the requested bucket does not exist, or you do not have permission to access it.`
557 | )
558 | }
559 | }
560 |
561 | protected static async initBucket(
562 | options: InitStoreOptions
563 | ): Promise {
564 | const { api, tomeShip, space, app, agent, bucket, type, isLog, perm } =
565 | options
566 | const action = `init-${type}`
567 | const body = {
568 | [action]: {
569 | ship: tomeShip,
570 | space,
571 | app,
572 | bucket,
573 | perm,
574 | },
575 | }
576 | if (type === 'feed') {
577 | // @ts-expect-error
578 | body[action].log = isLog
579 | }
580 | await api.poke({
581 | app: agent,
582 | mark: tomeMark,
583 | json: body,
584 | onError: (error) => {
585 | throw new Error(
586 | `Tome-${type}: Initializing store on ship ${tomeShip} failed. Make sure the ship and Tome agent are both running.\nError: ${error}`
587 | )
588 | },
589 | })
590 | }
591 |
592 | protected static async startWatchingForeignBucket(
593 | options: InitStoreOptions
594 | ): Promise {
595 | const { api, tomeShip, space, app, agent, bucket, type, isLog } =
596 | options
597 | const action = `watch-${type}`
598 | const mark = type === 'kv' ? kvMark : feedMark
599 | const body = {
600 | [action]: {
601 | ship: tomeShip,
602 | space,
603 | app,
604 | bucket,
605 | },
606 | }
607 | if (type === 'feed') {
608 | // @ts-expect-error
609 | body[action].log = isLog
610 | }
611 | await api.poke({
612 | app: agent,
613 | mark,
614 | json: body,
615 | onError: (error) => {
616 | throw new Error(
617 | `Tome-${type}: Starting foreign store watch failed. Make sure the ship and Tome agent are both running.\nError: ${error}`
618 | )
619 | },
620 | })
621 | }
622 |
623 | // called by subclasses
624 | protected async getCurrentForeignPerms() {
625 | // do nothing if not actually foreign
626 | if (this.isOurStore()) {
627 | return
628 | }
629 | return await DataStore._getCurrentForeignPerms({
630 | api: this.api,
631 | tomeShip: this.tomeShip,
632 | space: this.space,
633 | app: this.app,
634 | bucket: this.bucket,
635 | type: this.type,
636 | isLog: this.isLog,
637 | })
638 | }
639 |
640 | private static async _getCurrentForeignPerms(
641 | options: InitStoreOptions
642 | ): Promise {
643 | const { api, tomeShip, space, app, agent, bucket, type, isLog } =
644 | options
645 | const action = `team-${type}`
646 | const mark = type === 'kv' ? kvMark : feedMark
647 | const body = {
648 | [action]: {
649 | ship: tomeShip,
650 | space,
651 | app,
652 | bucket,
653 | },
654 | }
655 | if (type === 'feed') {
656 | // @ts-expect-error
657 | body[action].log = isLog
658 | }
659 | await api.poke({
660 | app: agent,
661 | mark,
662 | json: body,
663 | onError: (error) => {
664 | throw new Error(
665 | `Tome-${type}: Starting permissions watch failed. Make sure the ship and Tome agent are both running.\nError: ${error}`
666 | )
667 | },
668 | })
669 | }
670 |
671 | // subscribe to all values in the store, and keep cache synced.
672 | protected async subscribeAll(): Promise {
673 | this.storeSubscriptionID = await this.api.subscribe({
674 | app: this.agent,
675 | path: this.dataPath(),
676 | err: () => {
677 | throw new Error(
678 | `Tome-${this.type}: the store being used has been removed, or your access has been revoked.`
679 | )
680 | },
681 | event: async (update: SubscribeUpdate) => {
682 | if (this.type === 'kv') {
683 | await this._handleKvUpdates(update)
684 | } else {
685 | await this._handleFeedUpdates(
686 | update as FeedlogEntry[] | FeedlogUpdate
687 | )
688 | }
689 | this.dataUpdateCallback()
690 | this.setLoaded(true)
691 | },
692 | quit: async () => await this.subscribeAll(),
693 | })
694 | }
695 |
696 | // TODO duplicate logic in KV ALL method
697 | protected getAllLocalValues(): void {
698 | if (this.type === 'kv') {
699 | const map = new Map()
700 | const len = localStorage.length
701 | const startIndex = this.localDataPrefix().length
702 | for (let i = 0; i < len; i++) {
703 | const key = localStorage.key(i)
704 | if (key.startsWith(this.localDataPrefix())) {
705 | const keyName = key.substring(startIndex) // get key without prefix
706 | map.set(keyName, JSON.parse(localStorage.getItem(key)))
707 | }
708 | }
709 | this.cache = map
710 | this.dataUpdateCallback()
711 | this.setLoaded(true)
712 | } else if (this.type === 'feed') {
713 | const feedlog = localStorage.getItem(this.localDataPrefix())
714 | if (feedlog !== null) {
715 | this.feedlog = JSON.parse(feedlog)
716 | this.feedlog.map((entry: FeedlogEntry) => {
717 | this.order.push(entry.id)
718 | return entry
719 | })
720 | this.dataUpdateCallback()
721 | }
722 | this.setLoaded(true)
723 | }
724 | }
725 |
726 | /**
727 | * Set new permission levels for a store after initialization.
728 | *
729 | * @param permissions the new permissions to set.
730 | */
731 | public async setPermissions(permissions: Perm): Promise {
732 | if (!this.isOurStore()) {
733 | throw new Error(
734 | `Tome-${this.type}: You can only set permissions on your own ship's store.`
735 | )
736 | }
737 | const action = `perm-${this.type}`
738 | const mark = this.type === 'kv' ? kvMark : feedMark
739 | const body = {
740 | [action]: {
741 | ship: this.tomeShip,
742 | space: this.space,
743 | app: this.app,
744 | bucket: this.bucket,
745 | perm: permissions,
746 | },
747 | }
748 | if (this.type === 'feed') {
749 | // @ts-expect-error
750 | body[action].log = this.isLog
751 | }
752 | await this.api.poke({
753 | app: this.agent,
754 | mark,
755 | json: body,
756 | onError: (error) => {
757 | throw new Error(
758 | `Tome-${this.type}: Updating permissions failed.\nError: ${error}`
759 | )
760 | },
761 | })
762 | }
763 |
764 | /**
765 | * Set permission level for a specific ship. This takes precedence over bucket-level permissions.
766 | *
767 | * @param ship The ship to set permissions for.
768 | * @param level The permission level to set.
769 | */
770 | public async inviteShip(ship: string, level: InviteLevel): Promise {
771 | if (!this.isOurStore()) {
772 | throw new Error(
773 | `Tome-${this.type}: You can only manage permissions on your own ship's store.`
774 | )
775 | }
776 | if (!ship.startsWith('~')) {
777 | ship = `~${ship}`
778 | }
779 | const action = `invite-${this.type}`
780 | const mark = this.type === 'kv' ? kvMark : feedMark
781 | const body = {
782 | [action]: {
783 | ship: this.tomeShip,
784 | space: this.space,
785 | app: this.app,
786 | bucket: this.bucket,
787 | guy: ship,
788 | level,
789 | },
790 | }
791 | if (this.type === 'feed') {
792 | // @ts-expect-error
793 | body[action].log = this.isLog
794 | }
795 | await this.api.poke({
796 | app: this.agent,
797 | mark,
798 | json: body,
799 | onError: (error) => {
800 | throw new Error(
801 | `Tome-${this.type}: Setting ship permissions failed.\nError: ${error}`
802 | )
803 | },
804 | })
805 | }
806 |
807 | /**
808 | * Block a specific ship from accessing this store.
809 | *
810 | * @param ship The ship to block.
811 | */
812 | public async blockShip(ship: string): Promise {
813 | await this.inviteShip(ship, 'block')
814 | }
815 |
816 | protected dataPath(key?: string): string {
817 | let path = `/${this.type}/${this.tomeShip}/${this.spaceForPath}/${this.app}/${this.bucket}/`
818 | if (this.type === 'feed') {
819 | path += this.isLog ? 'log/' : 'feed/'
820 | }
821 | path += 'data/'
822 | if (key) {
823 | path += `key/${key}`
824 | } else {
825 | path += 'all'
826 | }
827 | return path
828 | }
829 |
830 | protected localDataPrefix(key?: string): string {
831 | let type: string = this.type
832 | if (this.isLog) {
833 | type = 'log'
834 | }
835 | let path = `/tome-db/${type}/${this.app}/${this.bucket}/`
836 | if (key) {
837 | path += key
838 | }
839 | return path
840 | }
841 |
842 | protected permsPath(): string {
843 | let path = `/${this.type}/${this.tomeShip}/${this.spaceForPath}/${this.app}/${this.bucket}/`
844 | if (this.type === 'feed') {
845 | path += this.isLog ? 'log/' : 'feed/'
846 | }
847 | path += 'perm'
848 | return path
849 | }
850 |
851 | protected setReady(ready: boolean): void {
852 | if (ready !== this.ready) {
853 | this.ready = ready
854 | if (this.onReadyChange) {
855 | this.onReadyChange(ready)
856 | }
857 | }
858 | }
859 |
860 | protected setLoaded(loaded: boolean): void {
861 | if (loaded !== this.loaded) {
862 | this.loaded = loaded
863 | if (this.onLoadChange) {
864 | this.onLoadChange(loaded)
865 | }
866 | }
867 | }
868 |
869 | protected setWrite(write: boolean): void {
870 | if (write !== this.write) {
871 | this.write = write
872 | if (this.onWriteChange) {
873 | this.onWriteChange(write)
874 | }
875 | }
876 | }
877 |
878 | protected setAdmin(admin: boolean): void {
879 | if (admin !== this.admin) {
880 | this.admin = admin
881 | if (this.onAdminChange) {
882 | this.onAdminChange(admin)
883 | }
884 | }
885 | }
886 |
887 | protected async waitForReady(): Promise {
888 | return await new Promise((resolve) => {
889 | while (!this.ready) {
890 | setTimeout(() => {}, 50)
891 | }
892 | resolve()
893 | })
894 | }
895 |
896 | protected async waitForLoaded(): Promise {
897 | return await new Promise((resolve) => {
898 | while (!this.loaded) {
899 | setTimeout(() => {}, 50)
900 | }
901 | resolve()
902 | })
903 | }
904 |
905 | protected canStore(value: any): boolean {
906 | if (
907 | value.constructor === Array ||
908 | value.constructor === Object ||
909 | value.constructor === String ||
910 | value.constructor === Number ||
911 | value.constructor === Boolean
912 | ) {
913 | return true
914 | }
915 | return false
916 | }
917 |
918 | protected dataUpdateCallback(): void {
919 | switch (this.type) {
920 | case 'kv':
921 | if (this.onDataChange) {
922 | this.onDataChange(this.cache)
923 | }
924 | break
925 | case 'feed':
926 | if (this.onDataChange) {
927 | this.onDataChange(this.feedlog)
928 | }
929 | break
930 | }
931 | }
932 |
933 | // called when switching spaces
934 | protected wipeLocalValues(): void {
935 | this.cache.clear()
936 | this.order.length = 0
937 | this.feedlog.length = 0
938 | this.dataUpdateCallback()
939 | }
940 |
941 | protected parseFeedlogEntry(entry: FeedlogEntry): FeedlogEntry {
942 | entry.createdBy = entry.createdBy.slice(1)
943 | entry.updatedBy = entry.updatedBy.slice(1)
944 | // @ts-ignore
945 | entry.content = JSON.parse(entry.content)
946 | entry.links = Object.fromEntries(
947 | Object.entries(entry.links).map(([k, v]) => [
948 | k.slice(1),
949 | JSON.parse(v),
950 | ])
951 | )
952 | return entry
953 | }
954 |
955 | protected async pokeOrTunnel({ json, onSuccess, onError }) {
956 | await this.waitForReady()
957 | let success = false
958 | if (this.isOurStore()) {
959 | let result: any // what onSuccess or onError returns
960 | await this.api.poke({
961 | app: this.agent,
962 | mark: this.type === 'kv' ? kvMark : feedMark,
963 | json,
964 | onSuccess: () => {
965 | result = onSuccess()
966 | },
967 | onError: () => {
968 | result = onError()
969 | },
970 | })
971 | return result
972 | } else {
973 | // Tunnel poke to Tome ship
974 | try {
975 | const result = await this.api.thread({
976 | inputMark: 'json',
977 | outputMark: 'json',
978 | threadName: this.type === 'kv' ? kvThread : feedThread,
979 | body: {
980 | ship: this.tomeShip,
981 | agent: this.agent,
982 | json: JSON.stringify(json),
983 | },
984 | desk: this.agent,
985 | })
986 | success = result === 'success'
987 | } catch (e) {}
988 | }
989 | return success ? onSuccess() : onError()
990 | }
991 |
992 | private async _handleKvUpdates(update: object): Promise {
993 | const entries: Array<[string, string]> = Object.entries(update)
994 | if (entries.length === 0) {
995 | // received an empty object, clear the cache.
996 | this.cache.clear()
997 | } else {
998 | for (const [key, value] of entries) {
999 | if (value === null) {
1000 | this.cache.delete(key)
1001 | } else {
1002 | this.cache.set(key, JSON.parse(value))
1003 | }
1004 | }
1005 | }
1006 | }
1007 |
1008 | private _handleFeedAll(update: FeedlogEntry[]): void {
1009 | update.map((entry: FeedlogEntry) => {
1010 | // save the IDs in time order so they are easier to find later
1011 | this.order.push(entry.id)
1012 | return this.parseFeedlogEntry(entry)
1013 | })
1014 | this.feedlog = update
1015 | }
1016 |
1017 | private async _handleFeedUpdate(update: FeedlogUpdate): Promise {
1018 | await this.waitForLoaded()
1019 | // %new, %edit, %delete, %clear, %set-link, %remove-link
1020 | let index: number
1021 | switch (update.type) {
1022 | case 'new': {
1023 | this.order.unshift(update.body.id)
1024 | const ship = update.body.ship.slice(1)
1025 | const entry = {
1026 | id: update.body.id,
1027 | createdAt: update.body.time,
1028 | updatedAt: update.body.time,
1029 | createdBy: ship,
1030 | updatedBy: ship,
1031 | // @ts-ignore
1032 | content: JSON.parse(update.body.content),
1033 | links: {},
1034 | }
1035 | this.feedlog.unshift(entry)
1036 | break
1037 | }
1038 | case 'edit':
1039 | index = this.order.indexOf(update.body.id)
1040 | if (index > -1) {
1041 | this.feedlog[index] = {
1042 | ...this.feedlog[index],
1043 | // @ts-ignore
1044 | content: JSON.parse(update.body.content),
1045 | updatedAt: update.body.time,
1046 | updatedBy: update.body.ship.slice(1),
1047 | }
1048 | }
1049 | break
1050 | case 'delete':
1051 | index = this.order.indexOf(update.body.id)
1052 | if (index > -1) {
1053 | this.feedlog.splice(index, 1)
1054 | this.order.splice(index, 1)
1055 | }
1056 | break
1057 | case 'clear':
1058 | this.wipeLocalValues()
1059 | break
1060 | case 'set-link':
1061 | index = this.order.indexOf(update.body.id)
1062 | if (index > -1) {
1063 | this.feedlog[index] = {
1064 | ...this.feedlog[index],
1065 | links: {
1066 | ...this.feedlog[index].links,
1067 | [update.body.ship.slice(1)]: JSON.parse(
1068 | update.body.value
1069 | ),
1070 | },
1071 | }
1072 | }
1073 | break
1074 | case 'remove-link':
1075 | index = this.order.indexOf(update.body.id)
1076 | if (index > -1) {
1077 | this.feedlog[index] = {
1078 | ...this.feedlog[index],
1079 | // @ts-ignore
1080 | links: (({ [update.body.ship.slice(1)]: _, ...o }) =>
1081 | o)(this.feedlog[index].links), // remove data.body.ship
1082 | }
1083 | }
1084 | break
1085 | default:
1086 | console.error('Tome-feed: unknown update type')
1087 | }
1088 | }
1089 |
1090 | private async _handleFeedUpdates(
1091 | update: FeedlogEntry[] | FeedlogUpdate
1092 | ): Promise {
1093 | if (update.constructor === Array) {
1094 | this._handleFeedAll(update)
1095 | } else {
1096 | await this._handleFeedUpdate(update as FeedlogUpdate)
1097 | }
1098 | }
1099 | }
1100 |
1101 | export abstract class FeedlogStore extends DataStore {
1102 | name: 'feed' | 'log'
1103 |
1104 | constructor(options: InitStoreOptions) {
1105 | super(options)
1106 | options.isLog ? (this.name = 'log') : (this.name = 'feed')
1107 | }
1108 |
1109 | private async _postOrEdit(
1110 | content: Content,
1111 | id?: string
1112 | ): Promise {
1113 | const action = typeof id === 'undefined' ? 'new-post' : 'edit-post'
1114 | if (action === 'new-post') {
1115 | id = uuid()
1116 | } else {
1117 | if (!validate(id)) {
1118 | console.error('Invalid ID.')
1119 | return undefined
1120 | }
1121 | }
1122 | if (!this.canStore(content)) {
1123 | console.error('content is an invalid type.')
1124 | return undefined
1125 | }
1126 | const contentStr = JSON.stringify(content)
1127 | const json = {
1128 | [action]: {
1129 | ship: this.tomeShip,
1130 | space: this.space,
1131 | app: this.app,
1132 | bucket: this.bucket,
1133 | log: this.isLog,
1134 | id,
1135 | content: contentStr,
1136 | },
1137 | }
1138 | return await this.pokeOrTunnel({
1139 | json,
1140 | onSuccess: () => {
1141 | // cache somewhere?
1142 | return id
1143 | },
1144 | onError: () => {
1145 | console.error(
1146 | `Tome-${this.name}: Failed to save content to the ${this.name}. Checking perms...`
1147 | )
1148 | this.getCurrentForeignPerms()
1149 | return undefined
1150 | },
1151 | })
1152 | }
1153 |
1154 | /**
1155 | * Add a new post to the feedlog. Automatically stores the creation time and author.
1156 | *
1157 | * @param content The Content to post to the feedlog.
1158 | * Can be a string, number, boolean, Array, or JSON.
1159 | * @returns The post ID on success, undefined on failure.
1160 | */
1161 | public async post(content: Content): Promise {
1162 | if (!this.mars) {
1163 | const now = new Date().getTime()
1164 | const id = uuid()
1165 | this.order.unshift(id)
1166 | const entry = {
1167 | id,
1168 | createdAt: now,
1169 | updatedAt: now,
1170 | createdBy: this.ourShip,
1171 | updatedBy: this.ourShip,
1172 | content,
1173 | links: {},
1174 | }
1175 | this.feedlog.unshift(entry)
1176 | localStorage.setItem(
1177 | this.localDataPrefix(),
1178 | JSON.stringify(this.feedlog)
1179 | )
1180 | this.dataUpdateCallback()
1181 | return id
1182 | }
1183 | return await this._postOrEdit(content)
1184 | }
1185 |
1186 | /**
1187 | * Edit a post in the feedlog. Automatically stores the updated time and author.
1188 | *
1189 | * @param id The ID of the post to edit.
1190 | * @param newContent The newContent to replace with.
1191 | * Can be a string, number, boolean, Array, or JSON.
1192 | * @returns The post ID on success, undefined on failure.
1193 | */
1194 | public async edit(
1195 | id: string,
1196 | newContent: Content
1197 | ): Promise {
1198 | if (!this.mars) {
1199 | const index = this.order.indexOf(id)
1200 | if (index === -1) {
1201 | console.error('ID not found.')
1202 | return undefined
1203 | }
1204 | this.feedlog[index] = {
1205 | ...this.feedlog[index],
1206 | updatedAt: new Date().getTime(),
1207 | content: newContent,
1208 | }
1209 | localStorage.setItem(
1210 | this.localDataPrefix(),
1211 | JSON.stringify(this.feedlog)
1212 | )
1213 | this.dataUpdateCallback()
1214 | return id
1215 | } else {
1216 | return await this._postOrEdit(newContent, id)
1217 | }
1218 | }
1219 |
1220 | /**
1221 | * Delete a post from the feedlog. If the post with ID does not exist, returns true.
1222 | *
1223 | * @param id The ID of the post to delete.
1224 | * @returns true on success, false on failure.
1225 | */
1226 | public async delete(id: string): Promise {
1227 | if (!validate(id)) {
1228 | console.error('Invalid ID.')
1229 | return false
1230 | }
1231 | if (!this.mars) {
1232 | const index = this.order.indexOf(id)
1233 | if (index === -1) {
1234 | return true
1235 | }
1236 | this.order.splice(index, 1)
1237 | this.feedlog.splice(index, 1)
1238 | localStorage.setItem(
1239 | this.localDataPrefix(),
1240 | JSON.stringify(this.feedlog)
1241 | )
1242 | this.dataUpdateCallback()
1243 | return true
1244 | }
1245 | const json = {
1246 | 'delete-post': {
1247 | ship: this.tomeShip,
1248 | space: this.space,
1249 | app: this.app,
1250 | bucket: this.bucket,
1251 | log: this.isLog,
1252 | id,
1253 | },
1254 | }
1255 | return await this.pokeOrTunnel({
1256 | json,
1257 | onSuccess: () => {
1258 | // cache somewhere?
1259 | return true
1260 | },
1261 | onError: () => {
1262 | console.error(
1263 | `Tome-${this.name}: Failed to delete post from ${this.name}. Checking perms...`
1264 | )
1265 | this.getCurrentForeignPerms()
1266 | return false
1267 | },
1268 | })
1269 | }
1270 |
1271 | /**
1272 | * Clear all posts from the feedlog.
1273 | *
1274 | * @returns true on success, false on failure.
1275 | */
1276 | public async clear(): Promise {
1277 | if (!this.mars) {
1278 | this.wipeLocalValues()
1279 | localStorage.removeItem(this.localDataPrefix())
1280 | this.dataUpdateCallback()
1281 | return true
1282 | }
1283 | const json = {
1284 | 'clear-feed': {
1285 | ship: this.tomeShip,
1286 | space: this.space,
1287 | app: this.app,
1288 | bucket: this.bucket,
1289 | log: this.isLog,
1290 | },
1291 | }
1292 | return await this.pokeOrTunnel({
1293 | json,
1294 | onSuccess: () => {
1295 | // cache somewhere?
1296 | return true
1297 | },
1298 | onError: () => {
1299 | console.error(
1300 | `Tome-${this.name}: Failed to clear ${this.name}. Checking perms...`
1301 | )
1302 | this.getCurrentForeignPerms()
1303 | return false
1304 | },
1305 | })
1306 | }
1307 |
1308 | /**
1309 | * Get the post from the feedlog with the given ID.
1310 | *
1311 | * @param id The ID of the post to retrieve.
1312 | * @param allowCachedValue If true, will return the cached value if it exists.
1313 | * If false, will always fetch from Urbit. Defaults to true.
1314 | * @returns A FeedlogEntry on success, undefined on failure.
1315 | */
1316 | public async get(
1317 | id: string,
1318 | allowCachedValue: boolean = true
1319 | ): Promise {
1320 | if (!this.mars) {
1321 | throw new Error(
1322 | 'Tome: get() is not supported in local mode. Please create an issue on GitHub if you need this feature.'
1323 | )
1324 | }
1325 | if (!validate(id)) {
1326 | console.error('Invalid ID.')
1327 | return undefined
1328 | }
1329 | await this.waitForReady()
1330 | if (allowCachedValue) {
1331 | const index = this.order.indexOf(id)
1332 | if (index > -1) {
1333 | return this.feedlog[index]
1334 | }
1335 | }
1336 | if (this.preload) {
1337 | await this.waitForLoaded()
1338 | const index = this.order.indexOf(id)
1339 | if (index === -1) {
1340 | console.error(`id ${id} not found`)
1341 | return undefined
1342 | }
1343 | return this.feedlog[index]
1344 | } else {
1345 | return await this._getValueFromUrbit(id)
1346 | }
1347 | }
1348 |
1349 | /**
1350 | * Retrieve all posts from the feedlog, sorted by newest first.
1351 | *
1352 | * @param useCache If true, return the current cache instead of querying Urbit.
1353 | * Only relevant if preload was set to false. Defaults to false.
1354 | * @returns A FeedlogEntry on success, undefined on failure.
1355 | */
1356 | public async all(useCache: boolean = false): Promise {
1357 | if (!this.mars) {
1358 | const posts = localStorage.getItem(this.localDataPrefix())
1359 | if (posts === null) {
1360 | return undefined
1361 | }
1362 | return JSON.parse(posts)
1363 | }
1364 | await this.waitForReady()
1365 | if (this.preload) {
1366 | await this.waitForLoaded()
1367 | return this.feedlog
1368 | }
1369 | if (useCache) {
1370 | return this.feedlog
1371 | }
1372 | return await this._getAllFromUrbit()
1373 | }
1374 |
1375 | private async _getValueFromUrbit(id: string): Promise {
1376 | try {
1377 | let post = await this.api.scry({
1378 | app: this.agent,
1379 | path: this.dataPath(id),
1380 | })
1381 | if (post === null) {
1382 | return undefined
1383 | }
1384 | post = this.parseFeedlogEntry(post)
1385 | const index = this.order.indexOf(id)
1386 | if (index === -1) {
1387 | // TODO find the actual right place to insert this (based on times)?
1388 | this.order.unshift(id)
1389 | this.feedlog.unshift(post)
1390 | } else {
1391 | this.feedlog[index] = post
1392 | }
1393 | return post
1394 | } catch (e) {
1395 | throw new Error(
1396 | `Tome-${this.type}: the store being used has been removed, or your access has been revoked.`
1397 | )
1398 | }
1399 | }
1400 |
1401 | private async _getAllFromUrbit(): Promise {
1402 | try {
1403 | const data = await this.api.scry({
1404 | app: this.agent,
1405 | path: this.dataPath(),
1406 | })
1407 | // wipe and replace feedlog
1408 | this.order.length = 0
1409 | this.feedlog = data.map((entry: FeedlogEntry) => {
1410 | this.order.push(entry.id)
1411 | return this.parseFeedlogEntry(entry)
1412 | })
1413 | return this.feedlog
1414 | } catch (e) {
1415 | throw new Error(
1416 | `Tome-${this.type}: the store being used has been removed, or your access has been revoked.`
1417 | )
1418 | }
1419 | }
1420 |
1421 | // other useful methods
1422 | // iterator(page_size = 50) or equivalent for a paginated query
1423 | // since(time: xxx) - returns all posts since time?
1424 | }
1425 |
1426 | export class FeedStore extends FeedlogStore {
1427 | private async _setOrRemoveLink(
1428 | id: string,
1429 | content?: Content
1430 | ): Promise {
1431 | const action =
1432 | typeof content !== 'undefined'
1433 | ? 'set-post-link'
1434 | : 'remove-post-link'
1435 | if (action === 'set-post-link') {
1436 | if (!this.canStore(content)) {
1437 | console.error('value is an invalid type.')
1438 | return false
1439 | }
1440 | }
1441 | const json = {
1442 | [action]: {
1443 | ship: this.tomeShip,
1444 | space: this.space,
1445 | app: this.app,
1446 | bucket: this.bucket,
1447 | log: this.isLog,
1448 | id,
1449 | },
1450 | }
1451 | if (action === 'set-post-link') {
1452 | // @ts-expect-error
1453 | json[action].value = JSON.stringify(content)
1454 | }
1455 | return await this.pokeOrTunnel({
1456 | json,
1457 | onSuccess: () => {
1458 | // cache somewhere?
1459 | return true
1460 | },
1461 | onError: () => {
1462 | console.error(
1463 | `Tome-${this.name}: Failed to modify link in the ${this.name}.`
1464 | )
1465 | this.getCurrentForeignPerms()
1466 | return false
1467 | },
1468 | })
1469 | }
1470 |
1471 | /**
1472 | * Associate a link with the feed post corresponding to ID.
1473 | *
1474 | * @param id The ID of the post to link to.
1475 | * @param content The Content to associate with the post.
1476 | * @returns true on success, false on failure.
1477 | */
1478 | public async setLink(id: string, content: Content): Promise {
1479 | if (!validate(id)) {
1480 | console.error('Invalid ID.')
1481 | return false
1482 | }
1483 | if (!this.mars) {
1484 | const index = this.order.indexOf(id)
1485 | if (index === -1) {
1486 | console.error('Post does not exist.')
1487 | return false
1488 | }
1489 | this.feedlog[index] = {
1490 | ...this.feedlog[index],
1491 | links: {
1492 | ...this.feedlog[index].links,
1493 | [this.ourShip]: content,
1494 | },
1495 | }
1496 | localStorage.setItem(
1497 | this.localDataPrefix(),
1498 | JSON.stringify(this.feedlog)
1499 | )
1500 | this.dataUpdateCallback()
1501 | return true
1502 | }
1503 | return await this._setOrRemoveLink(id, content)
1504 | }
1505 |
1506 | /**
1507 | * Remove the current ship's link to the feed post corresponding to ID.
1508 | *
1509 | * @param id The ID of the post to remove the link from.
1510 | * @returns true on success, false on failure. If the post with ID does not exist, returns true.
1511 | */
1512 | public async removeLink(id: string): Promise {
1513 | if (!validate(id)) {
1514 | console.error('Invalid ID.')
1515 | return false
1516 | }
1517 | if (!this.mars) {
1518 | const index = this.order.indexOf(id)
1519 | if (index === -1) {
1520 | console.error('Post does not exist.')
1521 | return false
1522 | }
1523 | this.feedlog[index] = {
1524 | ...this.feedlog[index],
1525 | // @ts-expect-error
1526 | links: (({ [this.ourShip]: _, ...o }) => o)(
1527 | this.feedlog[index].links
1528 | ), // remove data.body.ship
1529 | }
1530 | localStorage.setItem(
1531 | this.localDataPrefix(),
1532 | JSON.stringify(this.feedlog)
1533 | )
1534 | this.dataUpdateCallback()
1535 | return true
1536 | }
1537 | return await this._setOrRemoveLink(id)
1538 | }
1539 | }
1540 |
1541 | export class LogStore extends FeedlogStore {}
1542 |
1543 | export class KeyValueStore extends DataStore {
1544 | /**
1545 | * Set a key-value pair in the store.
1546 | *
1547 | * @param key The key to set.
1548 | * @param value The value to associate with the key. Can be a string, number, boolean, Array, or JSON.
1549 | * @returns true on success, false on failure.
1550 | */
1551 | public async set(key: string, value: Value): Promise {
1552 | if (!key) {
1553 | console.error('missing key parameter')
1554 | return false
1555 | }
1556 | if (!this.canStore(value)) {
1557 | console.error('value is an invalid type.')
1558 | return false
1559 | }
1560 | const valueStr = JSON.stringify(value)
1561 |
1562 | if (!this.mars) {
1563 | try {
1564 | localStorage.setItem(this.localDataPrefix(key), valueStr)
1565 | this.cache.set(key, value)
1566 | this.dataUpdateCallback()
1567 | return true
1568 | } catch (error) {
1569 | console.error(error)
1570 | return false
1571 | }
1572 | } else {
1573 | const json = {
1574 | 'set-value': {
1575 | ship: this.tomeShip,
1576 | space: this.space,
1577 | app: this.app,
1578 | bucket: this.bucket,
1579 | key,
1580 | value: valueStr,
1581 | },
1582 | }
1583 | return await this.pokeOrTunnel({
1584 | json,
1585 | onSuccess: () => {
1586 | this.cache.set(key, value)
1587 | this.dataUpdateCallback()
1588 | return true
1589 | },
1590 | onError: () => {
1591 | console.error('Failed to set key-value pair in the Store.')
1592 | this.getCurrentForeignPerms()
1593 | return false
1594 | },
1595 | })
1596 | }
1597 | }
1598 |
1599 | /**
1600 | * Remove a key-value pair from the store.
1601 | *
1602 | * @param key The key to remove.
1603 | * @returns true on success, false on failure. If the key does not exist, returns true.
1604 | */
1605 | public async remove(key: string): Promise {
1606 | if (!key) {
1607 | console.error('missing key parameter')
1608 | return false
1609 | }
1610 | if (!this.mars) {
1611 | localStorage.removeItem(this.localDataPrefix(key))
1612 | this.cache.delete(key)
1613 | this.dataUpdateCallback()
1614 | return true
1615 | } else {
1616 | const json = {
1617 | 'remove-value': {
1618 | ship: this.tomeShip,
1619 | space: this.space,
1620 | app: this.app,
1621 | bucket: this.bucket,
1622 | key,
1623 | },
1624 | }
1625 | return await this.pokeOrTunnel({
1626 | json,
1627 | onSuccess: () => {
1628 | this.cache.delete(key)
1629 | this.dataUpdateCallback()
1630 | return true
1631 | },
1632 | onError: () => {
1633 | console.error(
1634 | 'Failed to remove key-value pair from the Store.'
1635 | )
1636 | this.getCurrentForeignPerms()
1637 | return false
1638 | },
1639 | })
1640 | }
1641 | }
1642 |
1643 | /**
1644 | * Discard all values in the store.
1645 | *
1646 | * @returns true on success, false on failure.
1647 | */
1648 | public async clear(): Promise {
1649 | if (!this.mars) {
1650 | // TODO - only clear certain keys
1651 | localStorage.clear()
1652 | this.cache.clear()
1653 | this.dataUpdateCallback()
1654 | return true
1655 | } else {
1656 | const json = {
1657 | 'clear-kv': {
1658 | ship: this.tomeShip,
1659 | space: this.space,
1660 | app: this.app,
1661 | bucket: this.bucket,
1662 | },
1663 | }
1664 | return await this.pokeOrTunnel({
1665 | json,
1666 | onSuccess: () => {
1667 | this.cache.clear()
1668 | this.dataUpdateCallback()
1669 | return true
1670 | },
1671 | onError: () => {
1672 | console.error('Failed to clear Store.')
1673 | this.getCurrentForeignPerms()
1674 | return false
1675 | },
1676 | })
1677 | }
1678 | }
1679 |
1680 | /**
1681 | * Get the value associated with a key in the store.
1682 | *
1683 | * @param key The key to retrieve.
1684 | * @param allowCachedValue Whether we can check for cached values first.
1685 | * If false, we will always check Urbit for the latest value. Default is true.
1686 | * @returns The value associated with the key, or undefined if the key does not exist.
1687 | */
1688 | public async get(
1689 | key: string,
1690 | allowCachedValue: boolean = true
1691 | ): Promise {
1692 | if (!key) {
1693 | console.error('missing key parameter')
1694 | return undefined
1695 | }
1696 |
1697 | if (!this.mars) {
1698 | const value = localStorage.getItem(this.localDataPrefix(key))
1699 | if (value === null) {
1700 | console.error(`key ${key} not found`)
1701 | return undefined
1702 | }
1703 | return JSON.parse(value)
1704 | } else {
1705 | await this.waitForReady()
1706 | // first check cache if allowed
1707 | if (allowCachedValue) {
1708 | const value = this.cache.get(key)
1709 | if (typeof value !== 'undefined') {
1710 | return value
1711 | }
1712 | }
1713 | if (this.preload) {
1714 | await this.waitForLoaded()
1715 | const value = this.cache.get(key)
1716 | if (typeof value === 'undefined') {
1717 | console.error(`key ${key} not found`)
1718 | }
1719 | return value
1720 | } else {
1721 | return await this._getValueFromUrbit(key)
1722 | }
1723 | }
1724 | }
1725 |
1726 | /**
1727 | * Get all key-value pairs in the store.
1728 | *
1729 | * @param useCache return the cache instead of querying Urbit. Only relevant if preload was set to false.
1730 | * @returns A map of all key-value pairs in the store.
1731 | */
1732 | public async all(useCache: boolean = false): Promise