Lecture 17 Asynchronous Programming

Motivation

JavaScript Is Single-Threaded

Diagram showing that normal JavaScript code runs on a single thread.

Normal JavaScript code only executes on a single thread. That means the lines of code execute one at a time.

But We Have Lots of Long-Running and Sporadic Tasks!

Diagram of the JavaScript runtime showing main JavaScript code and asynchronous systems like timers, DOM events, web fetches, and database or file access.
  • but we frequently need to make calls to external, long-running tasks or tasks that fire unpredictably in response to user inputs like timers, DOM events, web fetches (e.g., loading images, calling LLMs), and database and file accesses. If we did all of that work synchronously, i.e., in the main flow of the JS program, our code would halt waiting for the other tasks to resolve.
  • all these tasks can be run asynchronously, which means they can run independently from the rest of the JavaScript code. In the rest of this lecture we'll look at programming design patterns and abstractions for handling asynchronous code

Asynchrony Is a Double-Edged Sword

Benefits

  • Exploit available parallelism
  • Do 10 things are once, finish 10 times faster
  • Continue to be responsive while waiting for something slow

Costs

  • Have to present pending status to the user
  • Have to remember what we were waiting for
  • Asynchrony is confusing b/c it's not sequential
    • can't trace code sequentially,
      many possible sequences!
    • code is complex, hard to read
    • source of many bugs
    • hard to debug

We'll see in an upcoming lecture how reactive programming can help mitigate some of these downsides.

<script defer src="s3.js"></script> <!--page fetch continues while s3.js is fetched--> <!--s3.js executes after page is loaded--> <!--fetch in parallel, execute in sequence--> <script type="module" src="s3.js"></script> <!--implicitly deferred--> <script async src="s2.js"></script> <!--page fetch/exec continues while s2.js is fetched and executed--> <!--fetch/execute both in parallel—more parallel than defer--> -->

Polling

Example

We want to

Polling

			
			  let img=document.createElement('img');

			  //setting src triggers load
			  img.src='https://pictures.com/mine.jpg'
			  while (!img.complete) {
			  // twiddle my thumbs
			  }

			  init();
			
		  
  • this blocks forever 😢

The Main Thread Gets Stuck

Diagram showing the JavaScript runtime with the main JavaScript code stuck in a while loop.

The main thread never finishes executing that while loop, so the asynchronous image load stays blocked too.

Polling with setInterval()

			
			  let img = document.createElement('img');

			  img.src = 'https://pictures.com/mine.jpg';

			  let pollId = setInterval(() => {
			    if (img.complete) {
			      init();
			    }
			  }, 250);

			  doOtherStuff();
			
		  
  • Poll periodically instead of blocking in a while loop
  • Browser can keep doing other work between polls
  • But this still keeps checking over and over
  • And once image loads, it will keep calling init()

Stop Polling with clearInterval()

			
			  let img = document.createElement('img');

			  img.src = 'https://pictures.com/mine.jpg';

			  let pollId = setInterval(() => {
			    if (img.complete) {
			      clearInterval(pollId);
			      init();
			    }
			  }, 250);

			  doOtherStuff();
			
		  
  • clearInterval() stops future polling once condition is met
  • Now init() only runs once
  • Still more cumbersome and wasteful than being notified directly
  • This motivates event listeners and other async abstractions

Polling

Sometimes Necessary

  • “uncooperative” monitoring
  • e.g., keep track of whether a page on another web site has changed
  • it won't tell you, so you have to keep asking

Drawbacks

  • polling is coarse-grained. We don't know something's updated until we check
  • wasting time and energy when nothing changes

Events

Events

Example

We want to

Load Event Listener

			
			  let img=document.createElement('img');

			  img.addEventListener('load',init);
			  img.src='https://pictures.com/mine.jpg'

			  doOtherStuff(); //runs during img load
			
		  
  • load event is triggered when item finishes loading
  • event listener is a callback
  • system invokes callback when event occurs
  • Drawback: code does not execute in order you see
  • many opportunities to get confused

What's Wrong?

			
			  let img=document.createElement('img');

			  img.src='https://pictures.com/mine.jpg'
			  img.addEventListener('load',init);

			  doOtherStuff(); //runs during img load
			
		  

Race Conditions

  • Triggered load before adding listener
    • may finish before listening starts
    • won't ever hear event
  • Common problem in async programming
    • Things happen in an unexpected order
  • Very hard to debug
    • When rerun, different order happens

Callbacks

Callbacks

  • the callback pattern uses functions to hand execution control to the async task.
  • We've actually seen this callback pattern twice already!
  • what happens when we need to chain multiple async tasks?
setInterval(() => {
  button.textContent = ++i;
}, 500);

img.addEventListener('load', () => {
  init();
});

Chaining Async Callbacks

Let's say I'm building a social media app, and I have a database of users. I want to check whether my best friend's best friend is me or not!

Looking up a user in the database is an async API query.

function getUser(userId, callback) { ... }

And to do this lookup we'll need to make several async requests.

getUser("jopo", user => {
  getUser(user.bestFriendId, friend => {
    getUser(friend.bestFriendId, friendOfFriend => {
      console.log(friendOfFriend.name);
    });
  });
});
🤮 Callback Hell!

Promises

Can we denest our callbacks?

getUser("jopo", user => {
  getUser(user.bestFriendId, friend => {
    getUser(friend.bestFriendId, friendOfFriend => {
      console.log(friendOfFriend.name);
    });
  });
});

getUser("jopo", user => { getUser(user.bestFriendId, friend => { getUser(friend.bestFriendId, friendOfFriend => { console.log(friendOfFriend.name); }); }); });

If we reformat our code slightly differently, we can begin to see a linear chaining pattern that could maybe replace the nesting structure.

Promise

Goal

  • Define code syntax that looks how we think
  • look up me then look up my best friend then look up their best friend
  • Represent order of actions and intents to wait

Example

			
			  getUser("jopo") //returns a promise
			  .then(user => getUser(user.bestFriendId))
			  .then(friend => getUser(friend.bestFriendId))
			  .then(friendOfFriend => {
		    console.log(friendOfFriend.name);
			  });
			
		  

Using Promises

Promise

Turning a callback into a promise

  • Common use of Promise constructor is to wrap callback-based APIs
  • Make them simpler to use
			
			  function getUserPromise (userId) {
			      return new Promise(fulfiller => {
			          getUser(userId, fulfiller);
			      });
			  }

			  getUserPromise("jopo")
			  .then(user => console.log(user.name));
			
		  

Passing Results

  • Promise is “thenable”
  • then(f) says f is a callback to invoke when promise is fulfilled
    • f receives value of fulfilled promise
    • add any number of callbacks any time
  • No race condition:
    • Callback provided before fulfillment is invoked upon fulfillment
    • Callback provided after fulfillment is invoked immediately
			
			  p = delayed("hi",1000);
			  // promise resolves to "hi"
			  // after 1000ms

			  p.then(res => console.log(res));
			  // console outputs 1000ms later
			  ...lots of code
			  p.then(res => console.log("bye"));
			  // if p already fulfilled,
			  // callback executes immediately
			
		  

Chaining


			wait(1000)	//promise fulfilled after 1000ms
			.then(() => console.log(1))
			.then(() => console.log(2))
			.then(() => console.log(3))
		  
		
What gets output?
  1. delay, 1,2,3
  2. delay, 3,2,1
  3. 2, delay, 1, 3
  4. 3, 2, 1, delay

Chaining


			wait(1000)	//promise fulfilled after 1000ms
			.then(() => console.log(1))
			.then(console.log(2))
			.then(() => console.log(3))
		  
		
What gets output?
  1. delay, 1,2,3
  2. delay, 3,2,1
  3. 2, delay, 1, 3
  4. 3, 2, 1, delay

Chaining Results

  • then() callback can return a value (or a promise of one)
  • passed to the next then() callback in chain when available
  • more precisely, then() returns a new promise p providing that value
  • so next then() is invoked on p
  • p.then() passes on the value of p
			
			  //traditional evaluation style
			  y = f(g(h(x)));

			  //promise-based
			  Promise.resolve(x)
			  // promise with fulfilled value x
			  .then(h) //h receives x
			  .then(g) //g receives h(x)
			  .then(f) //f receives g(h(x))
			  .then(res => y=res);
			
		  

Chaining Results

Parallel Promises

  • Promise.all() tracks multiple promises running in parallel
  • Returns one promise
    • resolved when all its subpromises are
    • value is array of individual promises' values
    • rejects if any of its individuals do
  • Much clearer about intent than nested event callbacks
    • so much easier to debug
			
			  let p1=fetch(url1);
			  let p2=fetch(url2);
			  let p3=fetch(url3);
			  Promise.all([p1,p2,p3])
			  .then([c1,c2,c3] =>
			  {//process all 3 pieces},
			  err => {handle the error})
			
		  

Error Handling

  • Sometimes, an asynchronous computation wants to report failure or errors
    • Could just pass the error to then() callback
    • But the pattern is so common we design for it
  • Promise can be rejected instead of fulfilled
  • then() can take a second callback
    • invoked when promise rejected
    • receives value of rejected promise---usually error message
    • like first callback, can return a value or promise
  • whichever callback is invoked, returns promise to invokes next then()
			
			  fetch(url)
			  //returns a promise to provide
			  //   content found at that url
			  .then(content => {
			    //executes if fetch is fulfilled
			    console.log("received" + content);
			    return 0},
			  error => {
			    //executes if fetch is rejected
			    console.log("error: " + error)
			    return -1}
			  })
			  .then(res => console.log(res))
			  //logs 0 if fetch succeeded,
			  //     1 if it failed
			
		  

Summary

Some Other Methods

Async and Await

Async/Await is Syntactic Sugar for Promises

Promise

			
getUser("jopo") //returns a promise
.then(user => getUser(user.bestFriendId))
.then(friend => getUser(friend.bestFriendId))
.then(friendOfFriend => {
  console.log(friendOfFriend.name);
});
			
		  
  • Promises use callbacks that say what to do when the promise is fulfilled
  • But can only use the resolved value in the next callback.
    • E.g., how could we console user at the end?
  • Lots of syntactic noise with then chains and arrow functions.
  • What if we could change the syntax to make async programming even easier?

Async/Await

			
async function showFriendOfFriend() {
  const user = await getUser("jopo");
  const friend = await getUser(user.bestFriendId);
  const friendOfFriend =
    await getUser(friend.bestFriendId);
  console.log(user, friendOfFriend.name);
}
			
		  
  • async/await uses async functions that return Promises
  • But await gives us a value (e.g. user is a raw value, not a promise)
  • user can be used downstream anywhere!
  • await pauses the async function without blocking the main thread
  • We can even use await in loops!
    for (let i=1; i<=5; i++) {
      let result =
        await delayed('answer',5000);
      console.log(i + result);
    }

Compare/Contrast

Advanced Reactivity

Refresher: The Async Runtime Model

Diagram of the JavaScript runtime showing main JavaScript code and asynchronous systems like timers, DOM events, web fetches, and database or file access.

This is the model from earlier in the lecture: JavaScript code runs on the main thread, and asynchronous systems at the edges signal back when something finishes or when input arrives.

Async/Await Programming Model

Diagram of the async/await programming model.

The async/await solution is to push all the async events into our synchronous code.

Reactive Programming Separates Sync and Async

Diagram showing the world signaling a change to the program in a reactive model.

Reactive programming separates our code into a synchronous program piece and an async outside world that sends us information.

Reactive Programming Separates Sync and Async

Diagram showing reactive computations running in response to a change.

Reactive programming separates our code into a synchronous program piece and an async outside world that sends us information.

Reactive Programming Separates Sync and Async

Diagram showing reactive effects updating the outside world.

Reactive programming separates our code into a synchronous program piece and an async outside world that sends us information.

Reactive Programming Separates Sync and Async

Diagram showing a complete reactive programming model from world input through computations to effects.

Reactive programming separates our code into a synchronous program piece and an async outside world that sends us information.

Reactivity Is A Restricted Kind of Async

async/await

  • Very expressive. Can write arbitrary async code
  • Can interleave asynchronous work with other work
  • Useful when async tasks must be run in sequence
  • But hard to reason about! Execution order matters

Reactivity

  • Less expressive than general-purpose async
  • Focused on sync-async handoff
  • Reason about values and changes, not execution order

Signals

Signals are the modern way to implement reactive programming systems

Vue Aside: Options API vs Composition API

Options API


import { createApp } from "vue";

createApp({
  template: "#template",
  data() {
    return {
      price: 5,
      quantity: 2
    }
  },
  computed: {
    total() {
      return this.price * this.quantity
    }
  }
}).mount("#app")
		  

Composition API


import { createApp, ref, computed } from "vue"

function setup() {
  const price = ref(5)
  const quantity = ref(2)

  const total = computed(() => {
    return price.value * quantity.value
  })

  return { price, quantity, total }
}

createApp({
  template: "#template",
  setup
}).mount("#app")
		  

We'll use Composition API for reactivity because it exposes the reactive pieces directly and is more flexible. Prompt your AI to use it too.

The Composition API also makes reactive programs look more like conventional programs, similar to how async/await programs look kind of like conventional programs.

Group by Primitive vs Purpose

Diagram comparing how the Options API organizes code by reactive primitive and how the Composition API organizes code by purpose.

The Options API separates concerns by reactive primitive whereas the Composition API lets you group things by purpose.

Three Reactive Primitives


const price = ref(5)
const quantity = ref(2)

const total = computed(() =>
  price.value * quantity.value)

watchEffect(() => {
  document.title =
    `Total: ${total.value}`
})
		  
  • Signal: ref() in Vue
    • a value that can be mutated by the world
  • Computation: computed() in Vue
    • a pure derived value, computed by the program
  • Effect: watchEffect() in Vue
    • performs side effects (updating the DOM, making network requests, console logging, etc.)
    • returns nothing
    • runs whenever its signal or computation dependencies change

ref and computed feel a little bit like await in that they allow us to use normal JavaScript programming constructs to manipulate code with special semantics. Values and computations behave differently from normal JavaScript ones.

Signals separate mutation, computation, and side effects

Why Don't We See Many Effects?


const count = ref(0)

<template>
  <p>Count: {{ count }}</p>
</template>

// framework implementation
watchEffect(() => {
  paragraph.textContent =
    `Count: ${count.value}`
})
		  
  • Effects usually terminate a reactive computation
  • But in our code so far, we haven't written many!
  • Rendering the DOM is itself an effect
  • When you interpolate a reactive value into the template, Vue arranges to update that part of the DOM when the value changes
  • Conceptually, this is something like a watchEffect() attached to the rendered output

watch()

  • watch() is a more targeted cousin of watchEffect()
  • watchEffect() tracks whatever reactive values you read inside it
  • watch() lets you specify exactly what source to watch
  • Use it when you want an effect only for particular changes, often with access to old and new values

const query = ref('')

watch(query, (newQuery, oldQuery) => {
  console.log('changed from',
    oldQuery, 'to', newQuery)
  fetchResults(newQuery)
})
		  

Don't Mutate Signals in a watch/watchEffect

  • Do not usually write to a signal inside an effect
  • An effect can accidentally trigger itself and loop forever
  • A signal write should come from the world
    • user input
    • network response
    • timer
  • If you find yourself deriving one signal from another in an effect, that is often a computed() instead

const state = ref({
  todos: [ "Create slides", "Release HW" ],
  total: 0
})

// plausible, but dangerous:
// "keep total in sync with todos"
watchEffect(() => {
  state.value.total =
    state.value.todos.length
})

// better: derived state is a computation
const total = computed(() => {
  return state.value.todos.length
})

// use total.value when needed
		  

Reactive Dictionaries and Arrays

  • You can store objects and arrays inside a ref()
  • That works well for reactive dictionaries, lookup tables, lists, and collections
  • When you render an array with v-for, give each item a stable :key
  • Keys help Vue diff efficiently and preserve the identity of each rendered item

// setup
const scores = ref({
  ada: 10,
  grace: 12
})

const todos = ref([
  { id: 1, text: 'Sketch', done: false },
  { id: 2, text: 'Prototype', done: true }
])

scores.value.ada++
todos.value.push({
  id: 3, text: 'Ship', done: false
})

// template
<li v-for="todo in todos" :key="todo.id">
  {{ todo.text }}
</li>
		  

Template Unwrapping

  • Vue auto-unwraps refs and computeds when you use them in templates
  • That means you usually write {{ count }}, not {{ count.value }}
  • In JavaScript, though, you still access ref values with .value

const count = ref(0)
const doubled = computed(() =>
  count.value * 2)

// template
{{ count + 1 }}
{{ doubled }}

// JavaScript
count.value++
		  

How Signals Actually Work

Signals Model 0: Just ref()


const count = ref(0)

count.subscribe(() =>
  console.log(count.value))

count.value++

// logs: 1
		  

function ref(initial) {
  let value = initial
  let subs = new Set()

  return {
    subscribe(reaction) {
      subs.add(reaction)
    },

    get value() {
      return value
    },

    set value(newValue) {
      if (newValue === value) return
      value = newValue
      for (let sub of subs) {
        sub()
      }
    }
  }
}
		  

This is the smallest useful signal model.

A ref stores a value and calls subscribed observers whenever that value changes.

There is no separate watch abstraction yet. Subscribers are just plain functions.

Problem: a subscriber can only depend on one signal.

Signals Model 1: Depending on Multiple Signals


const first = ref("jo")
const last = ref("po")

watch([first, last], () => {
  console.log(
    first.value + last.value)
})

first.value = "th" // "thpo"
last.value = "eia" // "theia"
		  

function watch(deps, fn) {
  for (let dep of deps) {
    dep.subscribe(fn)
  }
}
		  

Problem: We want to subscribe to just (and only just) the dependencies we actually use!

And ideally we shouldn't have to see the dependency list at all.

We can introduce a new function, watch, that will take an explicit dependency list and subscribe to all of them.

Signals Model 2: Automatically Add Dependencies When They're Read


const first = ref("jo")
const last = ref("po")

watchEffect(() => {
  console.log(
    first.value + last.value)
})

first.value = "th" // "thpo"
last.value = "eia" // "theia"
		  

Intuition: When a running effect reads a signal, we should automatically track it as a dependency!

Signals Model 2: Automatically Add Dependencies When They're Read


let currEffect

function ref(initial) {
  let value = initial
  let subs = new Set()

  return {
    subscribe(reaction) {
      subs.add(reaction)
    },
    get value() {
      return value
    },
    set value(newValue) {
      if (newValue === value) return
      value = newValue
      for (let sub of subs) {
        sub()
      }
    }
  }
}
		  

function watchEffect(fn) {
}
		  

To automatically add dependencies, instead of having the effect itself subscribe to the signal, we'll have the signals trigger autosubscription. We do this by tracking the current effect where the signal is read and then subscribing the current effect to the signal. Finally, we now have to execute the effect immediately so we can autosubscribe. Otherwise we wouldn't be tracking anything!

Signals Model 2: Automatically Add Dependencies When They're Read


let currEffect

function ref(initial) {
  let value = initial
  let subs = new Set()

  return {
    subscribe(reaction) {
      subs.add(reaction)
    },
    get value() {
      return value
    },
    set value(newValue) {
      if (newValue === value) return
      value = newValue
      for (let sub of subs) {
        sub()
      }
    }
  }
}
		  

function watchEffect(fn) {
  const execute = () => {
    const prevEffect = currEffect
    currEffect = fn
    try {
      fn()
    } finally {
      currEffect = prevEffect
    }
  }
}
		  

To automatically add dependencies, instead of having the effect itself subscribe to the signal, we'll have the signals trigger autosubscription. We do this by tracking the current effect where the signal is read and then subscribing the current effect to the signal. Finally, we now have to execute the effect immediately so we can autosubscribe. Otherwise we wouldn't be tracking anything!

Signals Model 2: Automatically Add Dependencies When They're Read


let currEffect

function ref(initial) {
  let value = initial
  let subs = new Set()

  return {
    subscribe(reaction) {
      subs.add(reaction)
    },
    get value() {
      if (currEffect) {
        subs.add(currEffect)
      }
      return value
    },
    set value(newValue) {
      if (newValue === value) return
      value = newValue
      for (let sub of subs) {
        sub()
      }
    }
  }
}
		  

function watchEffect(fn) {
  const execute = () => {
    const prevEffect = currEffect
    currEffect = fn
    try {
      fn()
    } finally {
      currEffect = prevEffect
    }
  }
}
		  

To automatically add dependencies, instead of having the effect itself subscribe to the signal, we'll have the signals trigger autosubscription. We do this by tracking the current effect where the signal is read and then subscribing the current effect to the signal. Finally, we now have to execute the effect immediately so we can autosubscribe. Otherwise we wouldn't be tracking anything!

Signals Model 2: Automatically Add Dependencies When They're Read


let currEffect

function ref(initial) {
  let value = initial
  let subs = new Set()

  return {
    subscribe(reaction) {
      subs.add(reaction)
    },
    get value() {
      if (currEffect) {
        subs.add(currEffect)
      }
      return value
    },
    set value(newValue) {
      if (newValue === value) return
      value = newValue
      for (let sub of subs) {
        sub()
      }
    }
  }
}
		  

function watchEffect(fn) {
  const execute = () => {
    const prevEffect = currEffect
    currEffect = fn
    try {
      fn()
    } finally {
      currEffect = prevEffect
    }
  }

  execute()
}
		  

To automatically add dependencies, instead of having the effect itself subscribe to the signal, we'll have the signals trigger autosubscription. We do this by tracking the current effect where the signal is read and then subscribing the current effect to the signal. Finally, we now have to execute the effect immediately so we can autosubscribe. Otherwise we wouldn't be tracking anything!

Signals Model 2: Automatically Add Dependencies When They're Read


let currEffect

function ref(initial) {
  let value = initial
  let subs = new Set()

  return {
    subscribe(reaction) {
      subs.add(reaction)
    },
    get value() {
      if (currEffect) {
        subs.add(currEffect)
      }
      return value
    },
    set value(newValue) {
      if (newValue === value) return
      value = newValue
      for (let sub of subs) {
        sub()
      }
    }
  }
}
		  

function watchEffect(fn) {
  const execute = () => {
    const prevEffect = currEffect
    currEffect = fn
    try {
      fn()
    } finally {
      currEffect = prevEffect
    }
  }

  execute()
}
		  

To automatically add dependencies, instead of having the effect itself subscribe to the signal, we'll have the signals trigger autosubscription. We do this by tracking the current effect where the signal is read and then subscribing the current effect to the signal. Finally, we now have to execute the effect immediately so we can autosubscribe. Otherwise we wouldn't be tracking anything!

Signals Model 3: Naive computed


const first = ref("jo")
const last = ref("po")

const full = computed(() =>
  first.value + last.value)

watchEffect(() => {
  console.log(full.value)
})

first.value = "th" // "thpo"
last.value = "eia" // "theia"
		  

function computed(fn) {
  const cachedValue = ref()
  watchEffect(() => cachedValue.value = fn())
  return cachedValue
}
		  

We'll create a ref, cachedValue, that will store the output of the function. Then we'll create a watchEffect that updates this cached value. Because it calls fn, it depends on any signals that fn reads!

Signals Model 3 Full Implementation


function ref(initial) {
  let value = initial
  let subs = new Set()

  return {
    subscribe(reaction) {
      subs.add(reaction)
    },
    get value() {
      if (currEffect) {
        subs.add(currEffect)
      }
      return value
    },
    set value(newValue) {
      if (newValue === value) return
      value = newValue
      for (let sub of subs) {
        sub()
      }
    }
  }
}
		  

function watch(deps, fn) {
  for (let dep of deps) {
    dep.subscribe(fn)
  }
}
		  

let currEffect

function watchEffect(fn) {
  const execute = () => {
    const prevEffect = currEffect
    currEffect = fn
    try {
      fn()
    } finally {
      currEffect = prevEffect
    }
  }

  execute()
}
		  

function computed(fn) {
  const cachedValue = ref()
  watchEffect(() => cachedValue.value = fn())
  return cachedValue
}
		  

Missing Features

  • dynamic dependencies: dependencies can change each time code is run (eg conditionals)
  • lazy computeds: computeds aren't run unless they're needed by an effect whose dependencies are stale
  • glitch-freedom: avoid states like "thpo" where some, but not all, updates have happened

Graffiti

Graffiti

A general purpose backend for building social software.

Examples

Graffiti Objects

{ value: { content: "Hi Josh! 👋", myCoolProperty: { foo: "bar" }, }, actor: "did:plc:numtqzbw74lmrguyvpzq6uf5", allowed: ["did:plc:vaw4t3tn7qqc6iyt5lrgvy6t"], url: "graffiti:...", channels: ["designftw", "did:plc:vaw4t3tn7qqc6iyt5lrgvy6t"], }

Object values

		
			{
				content: "Party! Saturday at 8pm",
				published: Date.now(),
			}
		
	

Object values: Event

		
			{
				content: "Party! Saturday at 8pm",
				published: Date.now(),
				location: "77 Mass Ave, Cambridge, MA",
				startTime: new Date(2025, 4, 19, 20).getTime(),
				attachment: [
					{
					    type: "image",
					    url: "graffiti...",
						alt: "A fun party flyer that reads..."
					}
				]
			}
		
	

Object values: Reply

		
			{
				content: "This is cool!",
				inReplyTo: "graffiti:..."
			}
		
	

Object values: Profile

		
			{
				name: "Theia",
				pronouns: "she/her",
				icon: {
					type: "image",
					url: "graffiti...",
					alt: "A picture of Theia"
				},
				describes: "did:plc:numtqzbw74lmrguyvpzq6uf5"
			}
		
	

Object values: Add to Group chat

		
			{
				// Add Theia to the group chat
			    activity: "Add",
			    object: "did:plc:numtqzbw74lmrguyvpzq6uf5",
				target: "cool-group-chat",
			}
		
	

Interacting with Objects

You cannot delete someone else's objects!

...but you can create arbitrarily complex interactions with objects.

Example: Chat administrator deleting a message

		
			{
			    activity: "Delete",
			    object: "graffiti:...",
			}
		
	
Example

Why did we decide not to let actors modify other actor's objects?

Why not "the person who created the chat is the moderator of that chat", etc.?

"Total Reification"

Who can see an object?

Why Channels? Why not just schema?

To prevent acidentally getting objects meant for other chats, applications, etc.

They define the context the object is in.

Context Collapse

A cartoon with a turkey food fight with the caption 'You had to bring up politics'

Channels: A constant string

{ channels: ["designftw"] }

For representing a very specific topic.

Channels: "My Channel"

{ channels: [session.actor] }

Channels: "Object's channel"

{ channels: [object.url] }

Replies, reactions, bookmarks, etc. related to an object. Can create "threaded" replies.

Channels: Random string

	
		{ channels: [random()] }
	
	

For representing user-defined contexts like group chats, etc.

Channels: existing standard

{ channels: ["isbn:0-486-27557-4"] }
{ channels: ["zip:02144"] }

For representing books, movies, websites, physical places, etc.

Channels: create your own!

Whatever helps you organize objects into meaningful groups.

Channels: fuzzy access control

Allowed: hard access control

No Allowed

Anyone can see it (if they query the right channel)


		{
			value: { content: "Hello, world" },
			actor: "did:plc:numtqzbw74lmrguyvpzq6uf5",
			url: "graffiti:...",
			channels: ["designftw"],
		}

Empty Allowed

Only you can see the object. Useful for:


		{
			value: { content: "Hello, me" },
			actor: "did:plc:numtqzbw74lmrguyvpzq6uf5",
			url: "graffiti:...",
			channels: ["designftw"],
			allowed: [],
		}

Full Allowed

Only the actors listed can see the object. Useful for:


		{
			value: { content: "Hello, friend" },
			actor: "did:plc:numtqzbw74lmrguyvpzq6uf5",
			url: "graffiti:...",
			channels: ["designftw"],
			allowed: ["did:plc:vaw4t3tn7qqc6iyt5lrgvy6t"],
		}